/* Copyright 2018 by Michiel de Hoon.  All rights reserved.
 * This file is part of the Biopython distribution and governed by your
 * choice of the "Biopython License Agreement" or the "BSD 3-Clause License".
 * Please see the LICENSE file that should have been included as part of this
 * package.
 */



#define PY_SSIZE_T_CLEAN
#include "Python.h"
#include "float.h"


#define HORIZONTAL 0x1
#define VERTICAL 0x2
#define DIAGONAL 0x4
#define ENDPOINT 0x8
#define M_MATRIX 0x1
#define Ix_MATRIX 0x2
#define Iy_MATRIX 0x4
#define DONE 0x8
#define NONE 0x7

#define OVERFLOW_ERROR -1
#define MEMORY_ERROR -2

#define CHARINDEX(s) (c = s, c >= 'a' ? c - 'a' : c - 'A')

#define SAFE_ADD(t, s) \
{   term = t; \
    if (term > PY_SSIZE_T_MAX - s) { count = OVERFLOW_ERROR; goto exit; } \
    s += term; \
}


typedef enum {NeedlemanWunschSmithWaterman,
              Gotoh,
              WatermanSmithBeyer,
              Unknown} Algorithm;

typedef enum {Global, Local} Mode;

typedef struct {
    unsigned char trace : 4;
    unsigned char path : 4;
} Trace;

typedef struct {
    unsigned char Ix : 4;
    unsigned char Iy : 4;
} TraceGapsGotoh;

typedef struct {
    int* MIx;
    int* IyIx;
    int* MIy;
    int* IxIy;
} TraceGapsWatermanSmithBeyer;

static int _convert_single_letter(PyObject* item)
{
    int i;
    char letter = '\0';
    Py_buffer view;
#if PY_MAJOR_VERSION >= 3
    if (PyUnicode_Check(item)) {
        Py_UCS1* data;
        if (PyUnicode_READY(item) == -1) return -1;
        switch (PyUnicode_KIND(item)) {
            case PyUnicode_1BYTE_KIND: break;
            case PyUnicode_2BYTE_KIND:
            case PyUnicode_4BYTE_KIND:
            case PyUnicode_WCHAR_KIND:
                PyErr_SetString(PyExc_ValueError,
                                "expected an ASCII character");
                return -1;
            default:
                PyErr_SetString(PyExc_SystemError,
                                "unknown PyUnicode kind constant");
                return -1;
        }
        data = PyUnicode_1BYTE_DATA(item);
        letter = *((char*)(data));
    }
    else
#endif
    {
        if (!PyObject_CheckBuffer(item)
         || PyObject_GetBuffer(item, &view, PyBUF_FORMAT) == -1) {
            PyErr_SetString(PyExc_ValueError, "expected a single letter");
            return -1;
        }
        if (strcmp(view.format, "B") != 0 || view.len != 1) {
            PyBuffer_Release(&view);
            PyErr_SetString(PyExc_ValueError, "expected a single letter");
            return -1;
        }
        letter = *((char*)(view.buf));
        PyBuffer_Release(&view);
    }
    if (letter >= 'a' && letter <= 'z') i = letter - 'a';
    else if (letter >= 'A' && letter <= 'Z') i = letter - 'A';
    else {
        PyErr_SetString(PyExc_ValueError, "expected an ASCII character");
        return -1;
    }
    return i;
}

static PyObject*
_create_path(Trace** M, int i, int j) {
    PyObject* tuple;
    PyObject* row;
    PyObject* value;
    int path;
    const int ii = i;
    const int jj = j;
    int n = 1;
    int direction = 0;

    while (1) {
        path = M[i][j].path;
        if (!path) break;
        if (path != direction) {
            n++;
            direction = path;
        }
        switch (path) {
            case HORIZONTAL: j++; break;
            case VERTICAL: i++; break;
            case DIAGONAL: i++; j++; break;
        }
    }
    i = ii;
    j = jj;

    direction = 0;
    tuple = PyTuple_New(n);
    if (!tuple) return NULL;
    n = 0;
    while (1) {
        path = M[i][j].path;
        if (path != direction) {
            row = PyTuple_New(2);
            if (!row) break;
#if PY_MAJOR_VERSION >= 3
            value = PyLong_FromLong(i);
#else
            value = PyInt_FromLong(i);
#endif
            if (!value) {
                Py_DECREF(row); /* all references were stolen */
                break;
            }
            PyTuple_SET_ITEM(row, 0, value);
#if PY_MAJOR_VERSION >= 3
            value = PyLong_FromLong(j);
#else
            value = PyInt_FromLong(j);
#endif
            if (!value) {
                Py_DECREF(row); /* all references were stolen */
                break;
            }
            PyTuple_SET_ITEM(row, 1, value);
            PyTuple_SET_ITEM(tuple, n, row);
            n++;
            direction = path;
        }
        switch (path) {
            case HORIZONTAL: j++; break;
            case VERTICAL: i++; break;
            case DIAGONAL: i++; j++; break;
            default: return tuple;
        }
    }
    Py_DECREF(tuple); /* all references were stolen */
    return PyErr_NoMemory();
}

typedef struct {
    PyObject_HEAD
    Trace** M;
    union { TraceGapsGotoh** gotoh;
            TraceGapsWatermanSmithBeyer** waterman_smith_beyer; } gaps;
    int nA;
    int nB;
    int iA;
    int iB;
    Mode mode;
    Algorithm algorithm;
    Py_ssize_t length;
} PathGenerator;

static Py_ssize_t
PathGenerator_needlemanwunsch_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    Py_ssize_t term;
    Py_ssize_t count;
    Py_ssize_t temp;
    Py_ssize_t* counts;
    counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!counts) return MEMORY_ERROR;
    counts[0] = 1;
    for (j = 1; j <= nB; j++) {
        trace = M[0][j].trace;
        count = 0;
        if (trace & HORIZONTAL) SAFE_ADD(counts[j-1], count);
        counts[j] = count;
    }
    for (i = 1; i <= nA; i++) {
        trace = M[i][0].trace;
        count = 0;
        if (trace & VERTICAL) SAFE_ADD(counts[0], count);
        temp = counts[0];
        counts[0] = count;
        for (j = 1; j <= nB; j++) {
            trace = M[i][j].trace;
            count = 0;
            if (trace & HORIZONTAL) SAFE_ADD(counts[j-1], count);
            if (trace & VERTICAL) SAFE_ADD(counts[j], count);
            if (trace & DIAGONAL) SAFE_ADD(temp, count);
            temp = counts[j];
            counts[j] = count;
        }
    }
exit:
    PyMem_Free(counts);
    return count;
}

static Py_ssize_t
PathGenerator_smithwaterman_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    Py_ssize_t term;
    Py_ssize_t count;
    Py_ssize_t total = 0;
    Py_ssize_t temp;
    Py_ssize_t* counts;
    counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!counts) return MEMORY_ERROR;
    counts[0] = 1;
    for (j = 1; j <= nB; j++) counts[j] = 1;
    for (i = 1; i <= nA; i++) {
        temp = counts[0];
        counts[0] = 1;
        for (j = 1; j <= nB; j++) {
            trace = M[i][j].trace;
            count = 0;
            if (trace & DIAGONAL) SAFE_ADD(temp, count);
            if (M[i][j].trace & ENDPOINT) SAFE_ADD(count, total);
            if (trace & HORIZONTAL) SAFE_ADD(counts[j-1], count);
            if (trace & VERTICAL) SAFE_ADD(counts[j], count);
            temp = counts[j];
            if (count == 0) count = 1;
            counts[j] = count;
        }
    }
    count = total;
exit:
    PyMem_Free(counts);
    return count;
}

static Py_ssize_t
PathGenerator_gotoh_global_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsGotoh** gaps = self->gaps.gotoh;
    Py_ssize_t count = MEMORY_ERROR;
    Py_ssize_t term;
    Py_ssize_t M_temp;
    Py_ssize_t Ix_temp;
    Py_ssize_t Iy_temp;
    Py_ssize_t* M_counts = NULL;
    Py_ssize_t* Ix_counts = NULL;
    Py_ssize_t* Iy_counts = NULL;
    M_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!M_counts) goto exit;
    Ix_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!Ix_counts) goto exit;
    Iy_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!Iy_counts) goto exit;
    M_counts[0] = 1;
    Ix_counts[0] = 0;
    Iy_counts[0] = 0;
    for (j = 1; j <= nB; j++) {
        M_counts[j] = 0;
        Ix_counts[j] = 0;
        Iy_counts[j] = 1;
    }
    for (i = 1; i <= nA; i++) {
        M_temp = M_counts[0];
        M_counts[0] = 0;
        Ix_temp = Ix_counts[0];
        Ix_counts[0] = 1;
        Iy_temp = Iy_counts[0];
        Iy_counts[0] = 0;
        for (j = 1; j <= nB; j++) {
            count = 0;
            trace = M[i][j].trace;
            if (trace & M_MATRIX) SAFE_ADD(M_temp, count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_temp, count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_temp, count);
            M_temp = M_counts[j];
            M_counts[j] = count;
            count = 0;
            trace = gaps[i][j].Ix;
            if (trace & M_MATRIX) SAFE_ADD(M_temp, count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_counts[j], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_counts[j], count);
            Ix_temp = Ix_counts[j];
            Ix_counts[j] = count;
            count = 0;
            trace = gaps[i][j].Iy;
            if (trace & M_MATRIX) SAFE_ADD(M_counts[j-1], count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_counts[j-1], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_counts[j-1], count);
            Iy_temp = Iy_counts[j];
            Iy_counts[j] = count;
        }
    }
    count = 0;
    if (M[nA][nB].trace) SAFE_ADD(M_counts[nB], count);
    if (gaps[nA][nB].Ix) SAFE_ADD(Ix_counts[nB], count);
    if (gaps[nA][nB].Iy) SAFE_ADD(Iy_counts[nB], count);
exit:
    if (M_counts) PyMem_Free(M_counts);
    if (Ix_counts) PyMem_Free(Ix_counts);
    if (Iy_counts) PyMem_Free(Iy_counts);
    return count;
}

static Py_ssize_t
PathGenerator_gotoh_local_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsGotoh** gaps = self->gaps.gotoh;
    Py_ssize_t term;
    Py_ssize_t count = MEMORY_ERROR;
    Py_ssize_t total = 0;
    Py_ssize_t M_temp;
    Py_ssize_t Ix_temp;
    Py_ssize_t Iy_temp;
    Py_ssize_t* M_counts = NULL;
    Py_ssize_t* Ix_counts = NULL;
    Py_ssize_t* Iy_counts = NULL;
    M_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!M_counts) goto exit;
    Ix_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!Ix_counts) goto exit;
    Iy_counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!Iy_counts) goto exit;
    M_counts[0] = 1;
    Ix_counts[0] = 0;
    Iy_counts[0] = 0;
    for (j = 1; j <= nB; j++) {
        M_counts[j] = 1;
        Ix_counts[j] = 0;
        Iy_counts[j] = 0;
    }
    for (i = 1; i <= nA; i++) {
        M_temp = M_counts[0];
        M_counts[0] = 1;
        Ix_temp = Ix_counts[0];
        Ix_counts[0] = 0;
        Iy_temp = Iy_counts[0];
        Iy_counts[0] = 0;
        for (j = 1; j <= nB; j++) {
            count = 0;
            trace = M[i][j].trace;
            if (trace & M_MATRIX) SAFE_ADD(M_temp, count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_temp, count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_temp, count);
            if (count == 0) count = 1;
            M_temp = M_counts[j];
            M_counts[j] = count;
            if (M[i][j].trace & ENDPOINT) SAFE_ADD(count, total);
            count = 0;
            trace = gaps[i][j].Ix;
            if (trace & M_MATRIX) SAFE_ADD(M_temp, count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_counts[j], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_counts[j], count);
            Ix_temp = Ix_counts[j];
            Ix_counts[j] = count;
            count = 0;
            trace = gaps[i][j].Iy;
            if (trace & M_MATRIX) SAFE_ADD(M_counts[j-1], count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_counts[j-1], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_counts[j-1], count);
            Iy_temp = Iy_counts[j];
            Iy_counts[j] = count;
        }
    }
    count = total;
exit:
    if (M_counts) PyMem_Free(M_counts);
    if (Ix_counts) PyMem_Free(Ix_counts);
    if (Iy_counts) PyMem_Free(Iy_counts);
    return count;
}

static Py_ssize_t
PathGenerator_waterman_smith_beyer_global_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    int* p;
    int gap;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsWatermanSmithBeyer** gaps = self->gaps.waterman_smith_beyer;
    Py_ssize_t count = MEMORY_ERROR;
    Py_ssize_t term;
    Py_ssize_t** M_count = NULL;
    Py_ssize_t** Ix_count = NULL;
    Py_ssize_t** Iy_count = NULL;
    M_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!M_count) goto exit;
    Ix_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!Ix_count) goto exit;
    Iy_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!Iy_count) goto exit;
    for (i = 0; i <= nA; i++) {
        M_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!M_count[i]) goto exit;
        Ix_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!Ix_count[i]) goto exit;
        Iy_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!Iy_count[i]) goto exit;
    }
    for (i = 0; i <= nA; i++) {
        for (j = 0; j <= nB; j++) {
            count = 0;
            trace = M[i][j].trace;
            if (trace & M_MATRIX) SAFE_ADD(M_count[i-1][j-1], count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_count[i-1][j-1], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_count[i-1][j-1], count);
            if (count == 0) count = 1; /* happens at M[0][0] only */
            M_count[i][j] = count;
            count = 0;
            p = gaps[i][j].MIx;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(M_count[i-gap][j], count);
                    p++;
                }
            }
            p = gaps[i][j].IyIx;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(Iy_count[i-gap][j], count);
                    p++;
                }
            }
            Ix_count[i][j] = count;
            count = 0;
            p = gaps[i][j].MIy;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(M_count[i][j-gap], count);
                    p++;
                }
            }
	    p = gaps[i][j].IxIy;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(Ix_count[i][j-gap], count);
                    p++;
                }
            }
            Iy_count[i][j] = count;
        }
    }
    count = 0;
    if (M[nA][nB].trace)
        SAFE_ADD(M_count[nA][nB], count);
    if (gaps[nA][nB].MIx[0] || gaps[nA][nB].IyIx[0])
        SAFE_ADD(Ix_count[nA][nB], count);
    if (gaps[nA][nB].MIy[0] || gaps[nA][nB].IxIy[0])
        SAFE_ADD(Iy_count[nA][nB], count);
exit:
    if (M_count) {
        if (Ix_count) {
            if (Iy_count) {
                for (i = 0; i <= nA; i++) {
                    if (!M_count[i]) break;
                    PyMem_Free(M_count[i]);
                    if (!Ix_count[i]) break;
                    PyMem_Free(Ix_count[i]);
                    if (!Iy_count[i]) break;
                    PyMem_Free(Iy_count[i]);
                }
                PyMem_Free(Iy_count);
            }
            PyMem_Free(Ix_count);
        }
        PyMem_Free(M_count);
    }
    return count;
}

static Py_ssize_t
PathGenerator_waterman_smith_beyer_local_length(PathGenerator* self)
{
    int i;
    int j;
    int trace;
    int* p;
    int gap;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsWatermanSmithBeyer** gaps = self->gaps.waterman_smith_beyer;
    Py_ssize_t term;
    Py_ssize_t count = MEMORY_ERROR;
    Py_ssize_t total = 0;
    Py_ssize_t** M_count = NULL;
    Py_ssize_t** Ix_count = NULL;
    Py_ssize_t** Iy_count = NULL;
    M_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!M_count) goto exit;
    Ix_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!Ix_count) goto exit;
    Iy_count = PyMem_Malloc((nA+1)*sizeof(Py_ssize_t*));
    if (!Iy_count) goto exit;
    for (i = 0; i <= nA; i++) {
        M_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!M_count[i]) goto exit;
        Ix_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!Ix_count[i]) goto exit;
        Iy_count[i] = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
        if (!Iy_count[i]) goto exit;
    }
    for (i = 0; i <= nA; i++) {
        for (j = 0; j <= nB; j++) {
            count = 0;
            trace = M[i][j].trace;
            if (trace & M_MATRIX) SAFE_ADD(M_count[i-1][j-1], count);
            if (trace & Ix_MATRIX) SAFE_ADD(Ix_count[i-1][j-1], count);
            if (trace & Iy_MATRIX) SAFE_ADD(Iy_count[i-1][j-1], count);
            if (count == 0) count = 1;
            M_count[i][j] = count;
            if (M[i][j].trace & ENDPOINT) SAFE_ADD(count, total);
            count = 0;
            p = gaps[i][j].MIx;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(M_count[i-gap][j], count);
                    p++;
                }
            }
            p = gaps[i][j].IyIx;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(Iy_count[i-gap][j], count);
                    p++;
                }
            }
            if (count == 0) count = 1;
            Ix_count[i][j] = count;
            count = 0;
            p = gaps[i][j].MIy;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(M_count[i][j-gap], count);
                    p++;
                }
            }
            p = gaps[i][j].IxIy;
            if (p) {
                while (1) {
                    gap = *p;
                    if (!gap) break;
                    SAFE_ADD(Ix_count[i][j-gap], count);
                    p++;
                }
            }
            if (count == 0) count = 1;
            Iy_count[i][j] = count;
        }
    }
    count = total;
exit:
    if (M_count) {
        if (Ix_count) {
            if (Iy_count) {
                for (i = 0; i <= nA; i++) {
                    if (!M_count[i]) break;
                    PyMem_Free(M_count[i]);
                    if (!Ix_count[i]) break;
                    PyMem_Free(Ix_count[i]);
                    if (!Iy_count[i]) break;
                    PyMem_Free(Iy_count[i]);
                }
                PyMem_Free(Iy_count);
            }
            PyMem_Free(Ix_count);
        }
        PyMem_Free(M_count);
    }
    return count;
}

static Py_ssize_t PathGenerator_length(PathGenerator* self) {
    Py_ssize_t length = self->length;
    if (length == 0) {
        switch (self->algorithm) {
            case NeedlemanWunschSmithWaterman:
                switch (self->mode) {
                    case Global:
                        length = PathGenerator_needlemanwunsch_length(self);
                        break;
                    case Local:
                        length = PathGenerator_smithwaterman_length(self);
                        break;
                    default:
                        /* should not happen, but some compilers complain that
                         * that length can be used uninitialized.
                         */
                        PyErr_SetString(PyExc_RuntimeError, "Unknown mode");
                        return -1;
                }
                break;
            case Gotoh:
                switch (self->mode) {
                    case Global:
                        length = PathGenerator_gotoh_global_length(self);
                        break;
                    case Local:
                        length = PathGenerator_gotoh_local_length(self);
                        break;
                    default:
                        /* should not happen, but some compilers complain that
                         * that length can be used uninitialized.
                         */
                        PyErr_SetString(PyExc_RuntimeError, "Unknown mode");
                        return -1;
                }
                break;
            case WatermanSmithBeyer:
                switch (self->mode) {
                    case Global:
                        length = PathGenerator_waterman_smith_beyer_global_length(self);
                        break;
                    case Local:
                        length = PathGenerator_waterman_smith_beyer_local_length(self);
                        break;
                    default:
                        /* should not happen, but some compilers complain that
                         * that length can be used uninitialized.
                         */
                        PyErr_SetString(PyExc_RuntimeError, "Unknown mode");
                        return -1;
                }
                break;
            case Unknown:
            default:
                PyErr_SetString(PyExc_RuntimeError, "Unknown algorithm");
                return -1;
        }
        self->length = length;
    }
    switch (length) {
        case OVERFLOW_ERROR:
            PyErr_Format(PyExc_OverflowError,
                         "number of optimal alignments is larger than %zd",
                         PY_SSIZE_T_MAX);
            break;
        case MEMORY_ERROR:
            PyErr_SetNone(PyExc_MemoryError);
            break;
        default:
            break;
    }
    return length;
}

static void
PathGenerator_dealloc(PathGenerator* self)
{
    int i;
    const int nA = self->nA;
    const Algorithm algorithm = self->algorithm;
    Trace** M = self->M;
    if (M) {
        for (i = 0; i <= nA; i++) {
            if (!M[i]) break;
            PyMem_Free(M[i]);
        }
        PyMem_Free(M);
    }
    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            break;
        case Gotoh: {
            TraceGapsGotoh** gaps = self->gaps.gotoh;
            if (gaps) {
                for (i = 0; i <= nA; i++) {
                    if (!gaps[i]) break;
                    PyMem_Free(gaps[i]);
                }
                PyMem_Free(gaps);
            }
            break;
        }
        case WatermanSmithBeyer: {
            TraceGapsWatermanSmithBeyer** gaps = self->gaps.waterman_smith_beyer;
            if (gaps) {
                int j;
                const int nB = self->nB;
                int* trace;
                for (i = 0; i <= nA; i++) {
                    if (!gaps[i]) break;
                    for (j = 0; j <= nB; j++) {
                        trace = gaps[i][j].MIx;
                        if (trace) PyMem_Free(trace);
                        trace = gaps[i][j].IyIx;
                        if (trace) PyMem_Free(trace);
                        trace = gaps[i][j].MIy;
                        if (trace) PyMem_Free(trace);
                        trace = gaps[i][j].IxIy;
                        if (trace) PyMem_Free(trace);
                    }
                    PyMem_Free(gaps[i]);
                }
                PyMem_Free(gaps);
            }
            break;
        }
        case Unknown:
        default:
            PyErr_WriteUnraisable((PyObject*)self);
            break;
    }
    Py_TYPE(self)->tp_free((PyObject*)self);
}

static PyObject* PathGenerator_next_needlemanwunsch(PathGenerator* self)
{
    int i = 0;
    int j = 0;
    int path;
    int trace = 0;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;

    path = M[i][j].path;
    if (path == DONE) return NULL;
    if (path == 0) {
        /* Generate the first path. */
        i = nA;
        j = nB;
    }
    else {
        /* We already have a path. Prune the path to see if there are
         * any alternative paths. */
        while (1) {
            if (path == HORIZONTAL) {
                trace = M[i][++j].trace;
                if (trace & VERTICAL) {
                    M[--i][j].path = VERTICAL;
                    break;
                }
                if (trace & DIAGONAL) {
                    M[--i][--j].path = DIAGONAL;
                    break;
                }
            }
            else if (path == VERTICAL) {
                trace = M[++i][j].trace;
                if (trace & DIAGONAL) {
                    M[--i][--j].path = DIAGONAL;
                    break;
                }
            }
            else /* DIAGONAL */ {
                i++;
                j++;
            }
            path = M[i][j].path;
            if (!path) {
                /* we reached the end of the alignment without finding
                 * an alternative path */
                M[0][0].path = DONE;
                return NULL;
            }
        }
    }
    /* Follow the traceback until we reach the origin. */
    while (1) {
        trace = M[i][j].trace;
        if (trace & HORIZONTAL) M[i][--j].path = HORIZONTAL;
        else if (trace & VERTICAL) M[--i][j].path = VERTICAL;
        else if (trace & DIAGONAL) M[--i][--j].path = DIAGONAL;
        else break;
    }
    return _create_path(M, 0, 0);
}

static PyObject* PathGenerator_next_smithwaterman(PathGenerator* self)
{
    int trace = 0;
    int i = self->iA;
    int j = self->iB;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    int path = M[0][0].path;

    if (path == DONE || path == NONE) return NULL;

    path = M[i][j].path;
    if (path) {
        /* We already have a path. Prune the path to see if there are
         * any alternative paths. */
        while (1) {
            if (path == HORIZONTAL) {
                trace = M[i][++j].trace;
                if (trace & VERTICAL) {
                    M[--i][j].path = VERTICAL;
                    break;
                }
                else if (trace & DIAGONAL) {
                    M[--i][--j].path = DIAGONAL;
                    break;
                }
            }
            else if (path == VERTICAL) {
                trace = M[++i][j].trace;
                if (trace & DIAGONAL) {
                    M[--i][--j].path = DIAGONAL;
                    break;
                }
            }
            else /* DIAGONAL */ {
                i++;
                j++;
            }
            path = M[i][j].path;
            if (!path) break;
        }
    }

    if (path) {
        trace = M[i][j].trace;
    } else {
        /* Find a suitable end point for a path.
         * Only allow start points ending at the M matrix. */
        while (1) {
            if (j < nB) j++;
            else if (i < nA) {
                i++;
                j = 0;
            }
            else {
                /* we reached the end of the sequences without finding
                 * an alternative path */
                M[0][0].path = DONE;
                return NULL;
            }
            trace = M[i][j].trace;
            if (trace & ENDPOINT) {
                trace &= DIAGONAL; /* exclude paths ending in a gap */
                break;
            }
        }
        M[i][j].path = 0;
    }

    /* Follow the traceback until we reach the origin. */
    while (1) {
        if (trace & HORIZONTAL) M[i][--j].path = HORIZONTAL;
        else if (trace & VERTICAL) M[--i][j].path = VERTICAL;
        else if (trace & DIAGONAL) M[--i][--j].path = DIAGONAL;
        else break;
        trace = M[i][j].trace;
    }
    self->iA = i;
    self->iB = j;
    return _create_path(M, i, j);
}

static PyObject* PathGenerator_next_gotoh_global(PathGenerator* self)
{
    int i = 0;
    int j = 0;
    int m;
    int path;
    int trace = 0;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsGotoh** gaps = self->gaps.gotoh;

    m = M_MATRIX;
    path = M[i][j].path;
    if (path == DONE) return NULL;
    if (path == 0) {
        i = nA;
        j = nB;
    }
    else {
        /* We already have a path. Prune the path to see if there are
         * any alternative paths. */
        while (1) {
            path = M[i][j].path;
            if (path == 0) {
                switch (m) {
                    case M_MATRIX: m = Ix_MATRIX; break;
                    case Ix_MATRIX: m = Iy_MATRIX; break;
                    case Iy_MATRIX: m = 0; break;
                }
                break;
            }
            switch (path) {
                case HORIZONTAL: trace = gaps[i][++j].Iy; break;
                case VERTICAL: trace = gaps[++i][j].Ix; break;
                case DIAGONAL: trace = M[++i][++j].trace; break;
            }
            switch (m) {
                case M_MATRIX:
                    if (trace & Ix_MATRIX) {
                        m = Ix_MATRIX;
                        break;
                    }
                case Ix_MATRIX:
                    if (trace & Iy_MATRIX) {
                        m = Iy_MATRIX;
                        break;
                    }
                case Iy_MATRIX:
                default:
                    switch (path) {
                        case HORIZONTAL: m = Iy_MATRIX; break;
                        case VERTICAL: m = Ix_MATRIX; break;
                        case DIAGONAL: m = M_MATRIX; break;
                    }
                    continue;
            }
            switch (path) {
                case HORIZONTAL: j--; break;
                case VERTICAL: i--; break;
                case DIAGONAL: i--; j--; break;
            }
            M[i][j].path = path;
            break;
        }
    }

    if (path == 0) {
        /* Generate a new path. */
        switch (m) {
            case M_MATRIX:
                if (M[nA][nB].trace) {
                   /* m = M_MATRIX; */
                   break;
                }
            case Ix_MATRIX:
                if (gaps[nA][nB].Ix) {
                   m = Ix_MATRIX;
                   break;
                }
            case Iy_MATRIX:
                if (gaps[nA][nB].Iy) {
                   m = Iy_MATRIX;
                   break;
                }
            default:
                /* exhausted this generator */
                M[0][0].path = DONE;
                return NULL;
        }
    }

    switch (m) {
        case M_MATRIX:
            trace = M[i][j].trace;
            path = DIAGONAL;
            i--; j--;
            break;
        case Ix_MATRIX:
            trace = gaps[i][j].Ix;
            path = VERTICAL;
            i--;
            break;
        case Iy_MATRIX:
            trace = gaps[i][j].Iy;
            path = HORIZONTAL;
            j--;
            break;
    }

    while (1) {
        if (trace & M_MATRIX) {
            trace = M[i][j].trace;
            M[i][j].path = path;
            path = DIAGONAL;
            i--; j--;
        }
        else if (trace & Ix_MATRIX) {
            M[i][j].path = path;
            trace = gaps[i][j].Ix;
            path = VERTICAL;
            i--;
        }
        else if (trace & Iy_MATRIX) {
            M[i][j].path = path;
            trace = gaps[i][j].Iy;
            path = HORIZONTAL;
            j--;
        }
        else break;
    }
    return _create_path(M, 0, 0);
}

static PyObject* PathGenerator_next_gotoh_local(PathGenerator* self)
{
    int trace = 0;
    int i;
    int j;
    int m = M_MATRIX;
    int iA = self->iA;
    int iB = self->iB;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsGotoh** gaps = self->gaps.gotoh;
    int path = M[0][0].path;

    if (path == DONE) return NULL;

    path = M[iA][iB].path;

    if (path) {
        i = iA;
        j = iB;
        while (1) {
            /* We already have a path. Prune the path to see if there are
             * any alternative paths. */
            path = M[i][j].path;
            if (path == 0) {
                m = M_MATRIX;
                iA = i;
                iB = j;
                break;
            }
            switch (path) {
                case HORIZONTAL: trace = gaps[i][++j].Iy; break;
                case VERTICAL: trace = gaps[++i][j].Ix; break;
                case DIAGONAL: trace = M[++i][++j].trace; break;
            }
            switch (m) {
                case M_MATRIX:
                    if (trace & Ix_MATRIX) {
                        m = Ix_MATRIX;
                        break;
                    }
                case Ix_MATRIX:
                    if (trace & Iy_MATRIX) {
                        m = Iy_MATRIX;
                        break;
                    }
                case Iy_MATRIX:
                default:
                    switch (path) {
                        case HORIZONTAL: m = Iy_MATRIX; break;
                        case VERTICAL: m = Ix_MATRIX; break;
                        case DIAGONAL: m = M_MATRIX; break;
                    }
                    continue;
            }
            switch (path) {
                case HORIZONTAL: j--; break;
                case VERTICAL: i--; break;
                case DIAGONAL: i--; j--; break;
            }
            M[i][j].path = path;
            break;
        }
    }

    if (path == 0) {
        /* Find the end point for a new path. */
        while (1) {
            if (iB < nB) iB++;
            else if (iA < nA) {
                iA++;
                iB = 0;
            }
            else {
                /* we reached the end of the alignment without finding
                 * an alternative path */
                M[0][0].path = DONE;
                return NULL;
            }
            if (M[iA][iB].trace & ENDPOINT) {
                M[iA][iB].path = 0;
                break;
            }
        }
        m = M_MATRIX;
        i = iA;
        j = iB;
    }

    while (1) {
        switch (m) {
            case M_MATRIX: trace = M[i][j].trace; break;
            case Ix_MATRIX: trace = gaps[i][j].Ix; break;
            case Iy_MATRIX: trace = gaps[i][j].Iy; break;
        }
        if (trace == 0) {
            self->iA = i;
            self->iB = j;
            return _create_path(M, i, j);
        }
        switch (m) {
            case M_MATRIX:
                path = DIAGONAL;
                i--;
                j--;
                break;
            case Ix_MATRIX:
                path = VERTICAL;
                i--;
                break;
            case Iy_MATRIX:
                path = HORIZONTAL;
                j--;
                break;
        }
        if (trace & M_MATRIX) m = M_MATRIX;
        else if (trace & Ix_MATRIX) m = Ix_MATRIX;
        else if (trace & Iy_MATRIX) m = Iy_MATRIX;
        M[i][j].path = path;
    }
    return NULL;
}

static PyObject*
PathGenerator_next_waterman_smith_beyer_global(PathGenerator* self)
{
    int i = 0, j = 0;
    int iA, iB;
    int trace;
    int* gapM;
    int* gapXY;

    int m = M_MATRIX;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsWatermanSmithBeyer** gaps = self->gaps.waterman_smith_beyer;

    int gap;
    int path = M[0][0].path;

    if (path == DONE) return NULL;

    if (path) {
        /* We already have a path. Prune the path to see if there are
         * any alternative paths. */
        while (1) {
            if (!path) {
                m <<= 1;
                break;
            }
            switch (path) {
                case HORIZONTAL:
                    iA = i;
                    iB = j;
                    while (M[i][iB].path == HORIZONTAL) iB++;
                    break;
                case VERTICAL:
                    iA = i;
                    while (M[iA][j].path == VERTICAL) iA++;
                    iB = j;
                    break;
                case DIAGONAL:
                    iA = i + 1;
                    iB = j + 1;
                    break;
                default:
                    PyErr_SetString(PyExc_RuntimeError,
                        "Unexpected path in PathGenerator_next_waterman_smith_beyer_global");
                    return NULL;
            }
            if (i == iA) { /* HORIZONTAL */
                gapM = gaps[iA][iB].MIy;
                gapXY = gaps[iA][iB].IxIy;
                if (m == M_MATRIX) {
                    gap = iB - j;
                    while (*gapM != gap) gapM++;
                    gapM++;
                    gap = *gapM;
                    if (gap) {
                        j = iB - gap;
                        while (j < iB) M[i][--iB].path = HORIZONTAL;
                        break;
                    }
                } else if (m == Ix_MATRIX) {
                    gap = iB - j;
                    while (*gapXY != gap) gapXY++;
                    gapXY++;
                }
                gap = *gapXY;
                if (gap) {
                    m = Ix_MATRIX;
                    j = iB - gap;
                    while (j < iB) M[i][--iB].path = HORIZONTAL;
                    break;
                }
                /* no alternative found; continue pruning */
                m = Iy_MATRIX;
                j = iB;
            }
            else if (j == iB) { /* VERTICAL */
                gapM = gaps[iA][iB].MIx;
                gapXY = gaps[iA][iB].IyIx;
                if (m == M_MATRIX) {
                    gap = iA - i;
                    while (*gapM != gap) gapM++;
                    gapM++;
                    gap = *gapM;
                    if (gap) {
                        i = iA - gap;
                        while (i < iA) M[--iA][j].path = VERTICAL;
                        break;
                    }
                } else if (m == Iy_MATRIX) {
                    gap = iA - i;
                    while (*gapXY != gap) gapXY++;
                    gapXY++;
                }
                gap = *gapXY;
                if (gap) {
                    m = Iy_MATRIX;
                    i = iA - gap;
                    while (i < iA) M[--iA][j].path = VERTICAL;
                    break;
                }
                /* no alternative found; continue pruning */
                m = Ix_MATRIX;
                i = iA;
            }
            else { /* DIAGONAL */
                i = iA - 1;
                j = iB - 1;
                trace = M[iA][iB].trace;
                switch (m) {
                    case M_MATRIX:
                        if (trace & Ix_MATRIX) {
                            m = Ix_MATRIX;
                            M[i][j].path = DIAGONAL;
                            break;
                        }
                    case Ix_MATRIX:
                        if (trace & Iy_MATRIX) {
                            m = Iy_MATRIX;
                            M[i][j].path = DIAGONAL;
                            break;
                        }
                    case Iy_MATRIX:
                    default:
                        /* no alternative found; continue pruning */
                        m = M_MATRIX;
                        i = iA;
                        j = iB;
                        path = M[i][j].path;
                        continue;
                }
                /* alternative found; build path until starting point */
                break;
            }
            path = M[i][j].path;
        }
    }

    if (!path) {
        /* Find a suitable end point for a path. */
        switch (m) {
            case M_MATRIX:
                if (M[nA][nB].trace) {
                    /* m = M_MATRIX; */
                    break;
                }
            case Ix_MATRIX:
                if (gaps[nA][nB].MIx[0] || gaps[nA][nB].IyIx[0]) {
                    m = Ix_MATRIX;
                    break;
                }
            case Iy_MATRIX:
                if (gaps[nA][nB].MIy[0] || gaps[nA][nB].IxIy[0]) {
                    m = Iy_MATRIX;
                    break;
                }
            default:
                M[0][0].path = DONE;
                return NULL;
        }
        i = nA;
        j = nB;
    }

    /* Follow the traceback until we reach the origin. */
    while (1) {
        switch (m) {
            case M_MATRIX:
                trace = M[i][j].trace;
                if (trace & M_MATRIX) m = M_MATRIX;
                else if (trace & Ix_MATRIX) m = Ix_MATRIX;
                else if (trace & Iy_MATRIX) m = Iy_MATRIX;
                else return _create_path(M, i, j);
                i--;
                j--;
                M[i][j].path = DIAGONAL;
                break;
            case Ix_MATRIX:
                gap = gaps[i][j].MIx[0];
                if (gap) m = M_MATRIX;
                else {
                    gap = gaps[i][j].IyIx[0];
                    m = Iy_MATRIX;
                }
                iA = i - gap;
                while (iA < i) M[--i][j].path = VERTICAL;
                M[i][j].path = VERTICAL;
                break;
            case Iy_MATRIX:
                gap = gaps[i][j].MIy[0];
                if (gap) m = M_MATRIX;
                else {
                    gap = gaps[i][j].IxIy[0];
                    m = Ix_MATRIX;
                }
                iB = j - gap;
                while (iB < j) M[i][--j].path = HORIZONTAL;
                M[i][j].path = HORIZONTAL;
                break;
        }
    }
}

static PyObject*
PathGenerator_next_waterman_smith_beyer_local(PathGenerator* self)
{
    int i, j, m;
    int trace = 0;
    int* gapM;
    int* gapXY;

    int iA = self->iA;
    int iB = self->iB;
    const int nA = self->nA;
    const int nB = self->nB;
    Trace** M = self->M;
    TraceGapsWatermanSmithBeyer** gaps = self->gaps.waterman_smith_beyer;

    int gap;
    int path = M[0][0].path;

    if (path == DONE) return NULL;
    m = 0;
    path = M[iA][iB].path;
    if (path) {
        /* We already have a path. Prune the path to see if there are
         * any alternative paths. */
        m = M_MATRIX;
        i = iA;
        j = iB;
        while (1) {
            path = M[i][j].path;
            switch (path) {
                case HORIZONTAL:
                    iA = i;
                    iB = j;
                    while (M[i][iB].path == HORIZONTAL) iB++;
                    break;
                case VERTICAL:
                    iA = i;
                    iB = j;
                    while (M[iA][j].path == VERTICAL) iA++;
                    break;
                case DIAGONAL:
                    iA = i + 1;
                    iB = j + 1;
                    break;
                default:
                    iA = -1;
                    break;
            }
            if (iA < 0) {
                m = 0;
                iA = i;
                iB = j;
                break;
            }
            if (i == iA) { /* HORIZONTAL */
                gapM = gaps[iA][iB].MIy;
                gapXY = gaps[iA][iB].IxIy;
                if (m == M_MATRIX) {
                    gap = iB - j;
                    while (*gapM != gap) gapM++;
                    gapM++;
                    gap = *gapM;
                    if (gap) {
                        j = iB - gap;
                        while (j < iB) M[i][--iB].path = HORIZONTAL;
                        break;
                    }
                } else if (m == Ix_MATRIX) {
                    gap = iB - j;
                    while (*gapXY != gap) gapXY++;
                    gapXY++;
                }
                gap = *gapXY;
                if (gap) {
                    m = Ix_MATRIX;
                    j = iB - gap;
                    M[i][j].path = HORIZONTAL;
                    while (iB > j) M[i][--iB].path = HORIZONTAL;
                    break;
                }
                /* no alternative found; continue pruning */
                m = Iy_MATRIX;
                j = iB;
            }
            else if (j == iB) { /* VERTICAL */
                gapM = gaps[iA][iB].MIx;
                gapXY = gaps[iA][iB].IyIx;
                if (m == M_MATRIX) {
                    gap = iA - i;
                    while (*gapM != gap) gapM++;
                    gapM++;
                    gap = *gapM;
                    if (gap) {
                        i = iA - gap;
                        while (i < iA) M[--iA][j].path = VERTICAL;
                        break;
                    }
                } else if (m == Iy_MATRIX) {
                    gap = iA - i;
                    while (*gapXY != gap) gapXY++;
                    gapXY++;
                }
                gap = *gapXY;
                if (gap) {
                    m = Iy_MATRIX;
                    i = iA - gap;
                    M[i][j].path = VERTICAL;
                    while (iA > i) M[--iA][j].path = VERTICAL;
                    break;
                }
                /* no alternative found; continue pruning */
                m = Ix_MATRIX;
                i = iA;
            }
            else { /* DIAGONAL */
                i = iA - 1;
                j = iB - 1;
                trace = M[iA][iB].trace;
                switch (m) {
                    case M_MATRIX:
                        if (trace & Ix_MATRIX) {
                            m = Ix_MATRIX;
                            M[i][j].path = DIAGONAL;
                            break;
                        }
                    case Ix_MATRIX:
                        if (trace & Iy_MATRIX) {
                            m = Iy_MATRIX;
                            M[i][j].path = DIAGONAL;
                            break;
                        }
                    case Iy_MATRIX:
                    default:
                        /* no alternative found; continue pruning */
                        m = M_MATRIX;
                        i = iA;
                        j = iB;
                        continue;
                }
                /* alternative found; build path until starting point */
                break;
            }
        }
    }
 
    if (m == 0) {
        /* We are at [nA][nB]. Find a suitable end point for a path. */
        while (1) {
            if (iB < nB) iB++;
            else if (iA < nA) {
                iA++;
                iB = 0;
            }
            else {
                /* exhausted this generator */
                M[0][0].path = DONE;
                return NULL;
            }
            if (M[iA][iB].trace & ENDPOINT) break;
        }
        M[iA][iB].path = 0;
        m = M_MATRIX;
        i = iA;
        j = iB;
    }

    /* Follow the traceback until we reach the origin. */
    while (1) {
        switch (m) {
            case Ix_MATRIX:
                gapM = gaps[i][j].MIx;
                gapXY = gaps[i][j].IyIx;
                iB = j;
                gap = *gapM;
                if (gap) m = M_MATRIX;
                else {
                    gap = *gapXY;
                    m = Iy_MATRIX;
                }
                iA = i - gap;
                while (i > iA) M[--i][iB].path = VERTICAL;
                break;
            case Iy_MATRIX:
                gapM = gaps[i][j].MIy;
                gapXY = gaps[i][j].IxIy;
                iA = i;
                gap = *gapM;
                if (gap) m = M_MATRIX;
                else {
                    gap = *gapXY;
                    m = Ix_MATRIX;
                }
                iB = j - gap;
                while (j > iB) M[iA][--j].path = HORIZONTAL;
                break;
            case M_MATRIX:
                iA = i-1;
                iB = j-1;
                trace = M[i][j].trace;
                if (trace & M_MATRIX) m = M_MATRIX;
                else if (trace & Ix_MATRIX) m = Ix_MATRIX;
                else if (trace & Iy_MATRIX) m = Iy_MATRIX;
                else {
                    self->iA = i;
                    self->iB = j;
                    return _create_path(M, i, j);
                }
                M[iA][iB].path = DIAGONAL;
                break;
        }
        i = iA;
        j = iB;
    }
}

static PyObject *
PathGenerator_next(PathGenerator* self)
{
    const Mode mode = self->mode;
    const Algorithm algorithm = self->algorithm;
    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    return PathGenerator_next_needlemanwunsch(self);
                case Local:
                    return PathGenerator_next_smithwaterman(self);
            }
        case Gotoh:
            switch (mode) {
                case Global:
                    return PathGenerator_next_gotoh_global(self);
                case Local:
                    return PathGenerator_next_gotoh_local(self);
            }
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    return PathGenerator_next_waterman_smith_beyer_global(self);
                case Local:
                    return PathGenerator_next_waterman_smith_beyer_local(self);
            }
        case Unknown:
        default:
            PyErr_SetString(PyExc_RuntimeError, "Unknown algorithm");
            return NULL;
    }
}

static const char PathGenerator_reset__doc__[] = "reset the iterator";

static PyObject*
PathGenerator_reset(PathGenerator* self)
{
    switch (self->mode) {
        case Local:
            self->iA = 0;
            self->iB = 0;
        case Global: {
            Trace** M = self->M;
            switch (self->algorithm) {
                case NeedlemanWunschSmithWaterman:
                case Gotoh: {
                    if (M[0][0].path != NONE) M[0][0].path = 0;
                    break;
                }
                case WatermanSmithBeyer: {
                    M[0][0].path = 0;
                    break;
                }
                case Unknown:
                default:
                    break;
            }
        }
    }
    Py_INCREF(Py_None);
    return Py_None;
}

static PyMethodDef PathGenerator_methods[] = {
    {"reset",
     (PyCFunction)PathGenerator_reset,
     METH_NOARGS,
     PathGenerator_reset__doc__
    },
    {NULL}  /* Sentinel */
};

static PySequenceMethods PathGenerator_as_sequence = {
    (lenfunc)PathGenerator_length,  /* sq_length */
    NULL,                           /* sq_concat */
    NULL,                           /* sq_repeat */
    NULL,                           /* sq_item */
    NULL,                           /* sq_ass_item */
    NULL,                           /* sq_contains */
    NULL,                           /* sq_inplace_concat */
    NULL,                           /* sq_inplace_repeat */
};

static PyTypeObject PathGenerator_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "Path generator",               /* tp_name */
    sizeof(PathGenerator),          /* tp_basicsize */
    0,                              /* tp_itemsize */
    (destructor)PathGenerator_dealloc,  /* tp_dealloc */
    0,                              /* tp_print */
    0,                              /* tp_getattr */
    0,                              /* tp_setattr */
    0,                              /* tp_reserved */
    0,                              /* tp_repr */
    0,                              /* tp_as_number */
    &PathGenerator_as_sequence,     /* tp_as_sequence */
    0,                              /* tp_as_mapping */
    0,                              /* tp_hash */
    0,                              /* tp_call */
    0,                              /* tp_str */
    0,                              /* tp_getattro */
    0,                              /* tp_setattro */
    0,                              /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,             /* tp_flags */
    0,                              /* tp_doc */
    0,                              /* tp_traverse */
    0,                              /* tp_clear */
    0,                              /* tp_richcompare */
    0,                              /* tp_weaklistoffset */
    PyObject_SelfIter,              /* tp_iter */
    (iternextfunc)PathGenerator_next,      /* tp_iternext */
    PathGenerator_methods,          /* tp_methods */
};

typedef struct {
    PyObject_HEAD
    Mode mode;
    Algorithm algorithm;
    double match;
    double mismatch;
    double epsilon;
    double target_open_gap_score;
    double target_extend_gap_score;
    double target_left_open_gap_score;
    double target_left_extend_gap_score;
    double target_right_open_gap_score;
    double target_right_extend_gap_score;
    double query_open_gap_score;
    double query_extend_gap_score;
    double query_left_open_gap_score;
    double query_left_extend_gap_score;
    double query_right_open_gap_score;
    double query_right_extend_gap_score;
    PyObject* target_gap_function;
    PyObject* query_gap_function;
    double substitution_matrix[26][26]; /* 26 letters in the alphabet */
    int* letters;
} Aligner;

static int
Aligner_init(Aligner *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"match", "mismatch", NULL};
    int i, j;
    const int n = 26;

    self->mode = Global;
    self->match = 1.0;
    self->mismatch = 0.0;
    self->epsilon = 1.e-6;
    self->target_open_gap_score = 0;
    self->target_extend_gap_score = 0;
    self->query_open_gap_score = 0;
    self->query_extend_gap_score = 0;
    self->target_left_open_gap_score = 0;
    self->target_left_extend_gap_score = 0;
    self->target_right_open_gap_score = 0;
    self->target_right_extend_gap_score = 0;
    self->query_left_open_gap_score = 0;
    self->query_left_extend_gap_score = 0;
    self->query_right_open_gap_score = 0;
    self->query_right_extend_gap_score = 0;
    self->target_gap_function = NULL;
    self->query_gap_function = NULL;
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|dd", kwlist,
                                     &self->match, &self->mismatch))
        return -1;
    for (i = 0; i < n; i++) {
        self->substitution_matrix[i][i] = self->match;
        for (j = 0; j < i; j++) {
            self->substitution_matrix[i][j] = self->mismatch;
            self->substitution_matrix[j][i] = self->mismatch;
        }
    }
    i = 'X' - 'A';
    self->substitution_matrix[i][i] = 0.0;
    self->letters = NULL;
    self->algorithm = Unknown;
    return 0;
}

static void
Aligner_dealloc(Aligner* self)
{   if (self->letters) PyMem_Free(self->letters);
    Py_XDECREF(self->target_gap_function);
    Py_XDECREF(self->query_gap_function);
    Py_TYPE(self)->tp_free((PyObject*)self);
}

static PyObject*
Aligner_repr(Aligner* self)
{
  const char text[] = "Pairwise aligner, implementing the Needleman-Wunsch, Smith-Waterman, Gotoh, and Waterman-Smith-Beyer global and local alignment algorithms";
#if PY_MAJOR_VERSION >= 3
  return PyUnicode_FromString(text);
#else
  return PyString_FromString(text);
#endif
}

static PyObject*
Aligner_str(Aligner* self)
{
    int n;
    char text[1024];
    char* p = text;
    n = sprintf(text, "Pairwise sequence aligner with parameters\n");
    p += n;
    if (self->letters) {
        n = sprintf(p, "  match/mismatch score: <substitution matrix>\n");
        p += n;
    } else {
        n = sprintf(p, "  match score: %f\n", self->match);
        p += n;
        n = sprintf(p, "  mismatch score: %f\n", self->mismatch);
        p += n;
    }
    if (self->target_gap_function) {
#if PY_MAJOR_VERSION >= 3
        n = sprintf(p, "  target gap function: %%R\n");
        p += n;
#else
        char* s;
        PyObject* representation = PyObject_Repr(self->target_gap_function);
        if (!representation) return PyErr_NoMemory();
        s = PyString_AsString(representation);
        n = sprintf(p, "  target gap function: %s\n", s);
        p += n;
        Py_DECREF(representation);
#endif
    }
    else {
        n = sprintf(p, "  target open gap score: %f\n",
                       self->target_open_gap_score);
        p += n;
        n = sprintf(p, "  target extend gap score: %f\n",
                       self->target_extend_gap_score);
        p += n;
        n = sprintf(p, "  target left open gap score: %f\n",
                       self->target_left_open_gap_score);
        p += n;
        n = sprintf(p, "  target left extend gap score: %f\n",
                       self->target_left_extend_gap_score);
        p += n;
        n = sprintf(p, "  target right open gap score: %f\n",
                       self->target_right_open_gap_score);
        p += n;
        n = sprintf(p, "  target right extend gap score: %f\n",
                       self->target_right_extend_gap_score);
        p += n;
    }
    if (self->query_gap_function) {
#if PY_MAJOR_VERSION >= 3
        n = sprintf(p, "  query gap function: %%R\n");
        p += n;
#else
        char* s;
        PyObject* representation = PyObject_Repr(self->query_gap_function);
        if (!representation) return PyErr_NoMemory();
        s = PyString_AsString(representation);
        n = sprintf(p, "  query gap function: %s\n", s);
        p += n;
        Py_DECREF(representation);
#endif
    }
    else {
        n = sprintf(p, "  query open gap score: %f\n",
                       self->query_open_gap_score);
        p += n;
        n = sprintf(p, "  query extend gap score: %f\n",
                       self->query_extend_gap_score);
        p += n;
        n = sprintf(p, "  query left open gap score: %f\n",
                       self->query_left_open_gap_score);
        p += n;
        n = sprintf(p, "  query left extend gap score: %f\n",
                       self->query_left_extend_gap_score);
        p += n;
        n = sprintf(p, "  query right open gap score: %f\n",
                       self->query_right_open_gap_score);
        p += n;
        n = sprintf(p, "  query right extend gap score: %f\n",
                       self->query_right_extend_gap_score);
        p += n;
    }
    switch (self->mode) {
        case Global: n = sprintf(p, "  mode: global\n"); break;
        case Local: n = sprintf(p, "  mode: local\n"); break;
    }
    p += n;
#if PY_MAJOR_VERSION >= 3
    if (self->target_gap_function || self->query_gap_function)
        return PyUnicode_FromFormat(text, self->target_gap_function, self->query_gap_function);
    else if (self->target_gap_function)
        return PyUnicode_FromFormat(text, self->target_gap_function);
    else if (self->query_gap_function)
        return PyUnicode_FromFormat(text, self->query_gap_function);
    else
        return PyUnicode_FromString(text);
#else
    return PyString_FromString(text);
#endif
}

static char Aligner_mode__doc__[] = "alignment mode ('global' or 'local')";

static PyObject*
Aligner_get_mode(Aligner* self, void* closure)
{   const char* message = NULL;
    switch (self->mode) {
        case Global: message = "global"; break;
        case Local: message = "local"; break;
    }
#if PY_MAJOR_VERSION >= 3
    return PyUnicode_FromString(message);
#else
    return PyString_FromString(message);
#endif
}

static int
Aligner_set_mode(Aligner* self, PyObject* value, void* closure)
{
#if PY_MAJOR_VERSION >= 3
    if (PyUnicode_Check(value)) {
#else
    char* mode;
    if (PyString_Check(value)) {
#endif
#if PY_MAJOR_VERSION >= 3
        if (PyUnicode_CompareWithASCIIString(value, "global") == 0) {
            self->mode = Global;
            return 0;
        }
        if (PyUnicode_CompareWithASCIIString(value, "local") == 0) {
            self->mode = Local;
            return 0;
        }
#else
        mode = PyString_AsString(value);
        if (strcmp(mode, "global") == 0) {
            self->mode = Global;
            return 0;
        }
        if (strcmp(mode, "local") == 0) {
            self->mode = Local;
            return 0;
        }
#endif
    }
    PyErr_SetString(PyExc_ValueError,
                    "invalid mode (expected 'global' or 'local'");
    return -1;
}

static char Aligner_match__doc__[] = "match score";

static PyObject*
Aligner_get_match(Aligner* self, void* closure)
{   if (self->letters) {
        PyErr_SetString(PyExc_ValueError, "using a substitution matrix");
        return NULL;
    }
    return PyFloat_FromDouble(self->match);
}

static int
Aligner_set_match(Aligner* self, PyObject* value, void* closure)
{   int i;
    const int n = 26;
    const double match = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) {
        PyErr_SetString(PyExc_ValueError, "invalid match score");
        return -1;
    }
    self->match = match;
    for (i = 0; i < n; i++) self->substitution_matrix[i][i] = match;
    i = 'X' - 'A';
    self->substitution_matrix[i][i] = 0.0;
    if (self->letters) {
        PyMem_Free(self->letters);
        self->letters = NULL;
    }
    return 0;
}

static char Aligner_mismatch__doc__[] = "mismatch score";

static PyObject*
Aligner_get_mismatch(Aligner* self, void* closure)
{   if (self->letters) {
        PyErr_SetString(PyExc_ValueError, "using a substitution matrix");
        return NULL;
    }
    return PyFloat_FromDouble(self->mismatch);
}

static int
Aligner_set_mismatch(Aligner* self, PyObject* value, void* closure)
{   int i, j;
    const int n = 26;
    const double mismatch = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) {
        PyErr_SetString(PyExc_ValueError, "invalid match score");
        return -1;
    }
    self->mismatch = mismatch;
    for (i = 0; i < n; i++) {
        for (j = 0; j < i; j++) {
            self->substitution_matrix[i][j] = mismatch;
            self->substitution_matrix[j][i] = mismatch;
        }
    }
    i = 'X' - 'A';
    for (j = 0; j < n; j++) {
        self->substitution_matrix[i][j] = 0;
        self->substitution_matrix[j][i] = 0;
    }
    if (self->letters) {
        PyMem_Free(self->letters);
        self->letters = NULL;
    }
    return 0;
}

static char Aligner_substitution_matrix__doc__[] = "substitution_matrix";

static PyObject*
Aligner_get_substitution_matrix(Aligner* self, void* closure)
{   if (!self->letters) {
        PyErr_SetString(PyExc_ValueError, "using affine gap scores");
        return NULL;
    }
    else {
        int i, j;
        const int n = 26;
        const int* letters = self->letters;
        PyObject* key = NULL;
        PyObject* value = NULL;
        PyObject* matrix = PyDict_New();
        if (!matrix) goto exit;
        for (i = 0; i < n; i++) {
            if (!letters[i]) continue;
            for (j = 0; j < n; j++) {
                if (!letters[j]) continue;
#if PY_MAJOR_VERSION >= 3
                key = Py_BuildValue("(CC)", 'A' + i, 'A' + j);
#else
                key = Py_BuildValue("(cc)", 'A' + i, 'A' + j);
#endif
                if (!key) goto exit;
                value = PyFloat_FromDouble(self->substitution_matrix[i][j]);
                if (!value) goto exit;
                if (PyDict_SetItem(matrix, key, value) == -1) goto exit;
                Py_DECREF(key);
                Py_DECREF(value);
                value = NULL;
            }
        }
        return matrix;
exit:
        Py_XDECREF(matrix);
        Py_XDECREF(key);
        Py_XDECREF(value);
        return NULL;
    }
}

static int
Aligner_set_substitution_matrix(Aligner* self, PyObject* values, void* closure)
{   int i, j;
    const int n = 26;
    PyObject* key;
    PyObject* value;
    Py_ssize_t pos = 0;
    double score;
    double substitution_matrix[26][26];
    int substitution_matrix_flags[26][26];
    int letters[26];
    PyObject* item;
    if (!PyDict_Check(values)) {
        PyErr_SetString(PyExc_ValueError, "expected a dictionary");
        return -1;
    }
    for (i = 0; i < n; i++) {
        letters[i] = 0;
        for (j = 0; j < n; j++) substitution_matrix_flags[i][j] = 0;
    }
    while (PyDict_Next(values, &pos, &key, &value)) {
        score = PyFloat_AsDouble(value);
        if (PyErr_Occurred()) {
            PyErr_SetString(PyExc_ValueError, "invalid score found");
            return -1;
        }
        if (!PyTuple_Check(key) || PyTuple_GET_SIZE(key) != 2) {
            PyErr_SetString(PyExc_ValueError,
                            "each key should be a tuple of two letters");
            return -1;
        }
        item = PyTuple_GET_ITEM(key, 0);
        i = _convert_single_letter(item);
        if (i < 0) return -1;
        item = PyTuple_GET_ITEM(key, 1);
        j = _convert_single_letter(item);
        if (j < 0) return -1;
        if (substitution_matrix_flags[i][j]) {
            i = 'A' + i;
            j = 'A' + j;
            PyErr_Format(PyExc_ValueError,
                         "score for (%c,%c) specified more than once (substitution matrix is case-insensitive)",
                         i, j);
            return -1;
        }
        substitution_matrix_flags[i][j] = 1;
        substitution_matrix[i][j] = score;
        letters[i] = 1;
        letters[j] = 1;
    }
    if (!self->letters) self->letters = PyMem_Malloc(n*sizeof(int));
    if (!self->letters) {
        PyErr_SetNone(PyExc_MemoryError);
        return -1;
    }
    /* No errors - store the new substitution matrix */
    for (i = 0; i < n; i++) {
        self->letters[i] = letters[i];
        for (j = 0; j < n; j++) {
            if (!letters[i] || !letters[j]) continue;
            if (substitution_matrix_flags[i][j])
                score = substitution_matrix[i][j];
            else if (substitution_matrix_flags[j][i])
                score = substitution_matrix[j][i];
            else
                score = 0;
            self->substitution_matrix[i][j] = score;
        }
    }
    return 0;
}

static char Aligner_gap_score__doc__[] = "gap score";

static PyObject*
Aligner_get_gap_score(Aligner* self, void* closure)
{   
    if (self->target_gap_function || self->query_gap_function) {
        if (self->target_gap_function != self->query_gap_function) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        Py_INCREF(self->target_gap_function);
        return self->target_gap_function;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_extend_gap_score
         || score != self->target_left_open_gap_score
         || score != self->target_left_extend_gap_score
         || score != self->target_right_open_gap_score
         || score != self->target_right_extend_gap_score
         || score != self->query_open_gap_score
         || score != self->query_extend_gap_score
         || score != self->query_left_open_gap_score
         || score != self->query_left_extend_gap_score
         || score != self->query_right_open_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_gap_score(Aligner* self, PyObject* value, void* closure)
{   if (PyCallable_Check(value)) {
        Py_XDECREF(self->target_gap_function);
        Py_XDECREF(self->query_gap_function);
        Py_INCREF(value);
        Py_INCREF(value);
        self->target_gap_function = value;
        self->query_gap_function = value;
    }
    else {
        const double score = PyFloat_AsDouble(value);
        if (PyErr_Occurred()) return -1;
        if (self->target_gap_function) {
            Py_DECREF(self->target_gap_function);
            self->target_gap_function = NULL;
        }
        if (self->query_gap_function) {
            Py_DECREF(self->query_gap_function);
            self->query_gap_function = NULL;
        }
        self->target_open_gap_score = score;
        self->target_extend_gap_score = score;
        self->target_left_open_gap_score = score;
        self->target_left_extend_gap_score = score;
        self->target_right_open_gap_score = score;
        self->target_right_extend_gap_score = score;
        self->query_open_gap_score = score;
        self->query_extend_gap_score = score;
        self->query_left_open_gap_score = score;
        self->query_left_extend_gap_score = score;
        self->query_right_open_gap_score = score;
        self->query_right_extend_gap_score = score;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_gap_score__doc__[] = "internal and end open gap score";

static PyObject*
Aligner_get_open_gap_score(Aligner* self, void* closure)
{   
    if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_left_open_gap_score
         || score != self->target_right_open_gap_score
         || score != self->query_open_gap_score
         || score != self->query_left_open_gap_score
         || score != self->query_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_open_gap_score = score;
    self->target_left_open_gap_score = score;
    self->target_right_open_gap_score = score;
    self->query_open_gap_score = score;
    self->query_left_open_gap_score = score;
    self->query_right_open_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_gap_score__doc__[] = "extend gap score";

static PyObject*
Aligner_get_extend_gap_score(Aligner* self, void* closure)
{   
    if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_extend_gap_score;
        if (score != self->target_left_extend_gap_score
         || score != self->target_right_extend_gap_score
         || score != self->query_extend_gap_score
         || score != self->query_left_extend_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_extend_gap_score = score;
    self->target_left_extend_gap_score = score;
    self->target_right_extend_gap_score = score;
    self->query_extend_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->query_right_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_internal_gap_score__doc__[] = "internal gap score";

static PyObject*
Aligner_get_internal_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_extend_gap_score
         || score != self->query_open_gap_score
         || score != self->query_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_internal_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_open_gap_score = score;
    self->target_extend_gap_score = score;
    self->query_open_gap_score = score;
    self->query_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_internal_open_gap_score__doc__[] = "internal open gap score";

static PyObject*
Aligner_get_internal_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->query_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_internal_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_open_gap_score = score;
    self->query_open_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_internal_extend_gap_score__doc__[] = "internal extend gap score";

static PyObject*
Aligner_get_internal_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_extend_gap_score;
        if (score != self->query_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_internal_extend_gap_score(Aligner* self, PyObject* value,
                                      void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_extend_gap_score = score;
    self->query_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_end_gap_score__doc__[] = "end gap score";

static PyObject*
Aligner_get_end_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_left_extend_gap_score
         || score != self->target_right_open_gap_score
         || score != self->target_right_extend_gap_score
         || score != self->query_left_open_gap_score
         || score != self->query_left_extend_gap_score
         || score != self->query_right_open_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_end_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_open_gap_score = score;
    self->target_left_extend_gap_score = score;
    self->target_right_open_gap_score = score;
    self->target_right_extend_gap_score = score;
    self->query_left_open_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->query_right_open_gap_score = score;
    self->query_right_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_end_open_gap_score__doc__[] = "end open gap score";

static PyObject*
Aligner_get_end_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_right_open_gap_score
         || score != self->query_left_open_gap_score
         || score != self->query_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_end_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_open_gap_score = score;
    self->target_right_open_gap_score = score;
    self->query_left_open_gap_score = score;
    self->query_right_open_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_end_extend_gap_score__doc__[] = "end extend gap score";

static PyObject*
Aligner_get_end_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_extend_gap_score;
        if (score != self->target_right_extend_gap_score
         || score != self->query_left_extend_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_end_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_extend_gap_score = score;
    self->target_right_extend_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->query_right_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_left_gap_score__doc__[] = "left gap score";

static PyObject*
Aligner_get_left_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_left_extend_gap_score
         || score != self->query_left_open_gap_score
         || score != self->query_left_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_left_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_open_gap_score = score;
    self->target_left_extend_gap_score = score;
    self->query_left_open_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_right_gap_score__doc__[] = "right gap score";

static PyObject*
Aligner_get_right_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_right_open_gap_score;
        if (score != self->target_right_extend_gap_score
         || score != self->query_right_open_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_right_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_right_open_gap_score = score;
    self->target_right_extend_gap_score = score;
    self->query_right_open_gap_score = score;
    self->query_right_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_left_open_gap_score__doc__[] = "left open gap score";

static PyObject*
Aligner_get_left_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->query_left_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_left_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_open_gap_score = score;
    self->query_left_open_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_left_extend_gap_score__doc__[] = "left extend gap score";

static PyObject*
Aligner_get_left_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_extend_gap_score;
        if (score != self->query_left_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_left_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_left_extend_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_right_open_gap_score__doc__[] = "right open gap score";

static PyObject*
Aligner_get_right_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_right_open_gap_score;
        if (score != self->query_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_right_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_right_open_gap_score = score;
    self->query_right_open_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_right_extend_gap_score__doc__[] = "right extend gap score";

static PyObject*
Aligner_get_right_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function || self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_right_extend_gap_score;
        if (score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_right_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->target_right_extend_gap_score = score;
    self->query_right_extend_gap_score = score;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_open_gap_score__doc__[] = "target open gap score";

static PyObject*
Aligner_get_target_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_left_open_gap_score
         || score != self->target_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_open_gap_score = score;
    self->target_left_open_gap_score = score;
    self->target_right_open_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_extend_gap_score__doc__[] = "target extend gap score";

static PyObject*
Aligner_get_target_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_extend_gap_score;
        if (score != self->target_left_extend_gap_score
         || score != self->target_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_extend_gap_score = score;
    self->target_left_extend_gap_score = score;
    self->target_right_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_gap_score__doc__[] = "target gap score";

static PyObject*
Aligner_get_target_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        Py_INCREF(self->target_gap_function);
        return self->target_gap_function;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_extend_gap_score
         || score != self->target_left_open_gap_score
         || score != self->target_left_extend_gap_score
         || score != self->target_right_open_gap_score
         || score != self->target_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_gap_score(Aligner* self, PyObject* value, void* closure)
{
    if (PyCallable_Check(value)) {
        Py_XDECREF(self->target_gap_function);
        Py_INCREF(value);
        self->target_gap_function = value;
    }
    else {
        const double score = PyFloat_AsDouble(value);
        if (PyErr_Occurred()) {
            PyErr_SetString(PyExc_ValueError,
                            "gap score should be numerical or callable");
            return -1;
        }
        self->target_open_gap_score = score;
        self->target_extend_gap_score = score;
        self->target_left_open_gap_score = score;
        self->target_left_extend_gap_score = score;
        self->target_right_open_gap_score = score;
        self->target_right_extend_gap_score = score;
        if (self->target_gap_function) {
            Py_DECREF(self->target_gap_function);
            self->target_gap_function = NULL;
        }
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_open_gap_score__doc__[] = "query gap open score";

static PyObject*
Aligner_get_query_open_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_open_gap_score;
        if (score != self->query_left_open_gap_score
         || score != self->query_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_open_gap_score = score;
    self->query_left_open_gap_score = score;
    self->query_right_open_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_extend_gap_score__doc__[] = "query gap extend score";

static PyObject*
Aligner_get_query_extend_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_extend_gap_score;
        if (score != self->query_left_extend_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_extend_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->query_right_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_gap_score__doc__[] = "query gap score";

static PyObject*
Aligner_get_query_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        Py_INCREF(self->query_gap_function);
        return self->query_gap_function;
    }
    else {
        const double score = self->query_open_gap_score;
        if (score != self->query_left_open_gap_score
         || score != self->query_right_open_gap_score
         || score != self->query_extend_gap_score
         || score != self->query_left_extend_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_gap_score(Aligner* self, PyObject* value, void* closure)
{   if (PyCallable_Check(value)) {
        Py_XDECREF(self->query_gap_function);
        Py_INCREF(value);
        self->query_gap_function = value;
    }
    else {
        const double score = PyFloat_AsDouble(value);
        if (PyErr_Occurred()) {
            PyErr_SetString(PyExc_ValueError,
                            "gap score should be numerical or callable");
            return -1;
        }
        self->query_open_gap_score = score;
        self->query_extend_gap_score = score;
        self->query_left_open_gap_score = score;
        self->query_left_extend_gap_score = score;
        self->query_right_open_gap_score = score;
        self->query_right_extend_gap_score = score;
        if (self->query_gap_function) {
            Py_DECREF(self->query_gap_function);
            self->query_gap_function = NULL;
        }
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_internal_open_gap_score__doc__[] = "target internal open gap score";

static PyObject*
Aligner_get_target_internal_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_open_gap_score);
}

static int
Aligner_set_target_internal_open_gap_score(Aligner* self,
                                           PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_open_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_internal_extend_gap_score__doc__[] = "target internal extend gap score";

static PyObject*
Aligner_get_target_internal_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_extend_gap_score);
}

static int
Aligner_set_target_internal_extend_gap_score(Aligner* self,
                                             PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_internal_gap_score__doc__[] = "target internal gap score";

static PyObject*
Aligner_get_target_internal_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_open_gap_score;
        if (score != self->target_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_internal_gap_score(Aligner* self, PyObject* value,
                                      void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_open_gap_score = score;
    self->target_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_end_gap_score__doc__[] = "target end gap score";

static PyObject*
Aligner_get_target_end_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_left_extend_gap_score
         || score != self->target_right_open_gap_score
         || score != self->target_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_end_gap_score(Aligner* self, PyObject* value, void* closure) {
    const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_open_gap_score = score;
    self->target_left_extend_gap_score = score;
    self->target_right_open_gap_score = score;
    self->target_right_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_end_open_gap_score__doc__[] = "target end open gap score";

static PyObject*
Aligner_get_target_end_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_end_open_gap_score(Aligner* self, PyObject* value,
                                      void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_open_gap_score = score;
    self->target_right_open_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_end_extend_gap_score__doc__[] = "target end extend gap score";

static PyObject*
Aligner_get_target_end_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_extend_gap_score;
        if (score != self->target_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_end_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_extend_gap_score = score;
    self->target_right_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_left_open_gap_score__doc__[] = "target left open score";

static PyObject*
Aligner_get_target_left_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_left_open_gap_score);
}

static int
Aligner_set_target_left_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_open_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_left_extend_gap_score__doc__[] = "target left extend score";

static PyObject*
Aligner_get_target_left_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_left_extend_gap_score);
}

static int
Aligner_set_target_left_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_left_gap_score__doc__[] = "target left score";

static PyObject*
Aligner_get_target_left_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_left_open_gap_score;
        if (score != self->target_left_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_left_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_left_open_gap_score = score;
    self->target_left_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_right_gap_score_open__doc__[] = "target right open score";

static PyObject*
Aligner_get_target_right_open_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_right_open_gap_score);
}

static int
Aligner_set_target_right_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_right_open_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_right_extend_gap_score__doc__[] = "target right extend score";

static PyObject*
Aligner_get_target_right_extend_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->target_right_extend_gap_score);
}

static int
Aligner_set_target_right_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_right_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_target_right_gap_score__doc__[] = "target right score";

static PyObject*
Aligner_get_target_right_gap_score(Aligner* self, void* closure)
{   if (self->target_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->target_right_open_gap_score;
        if (score != self->target_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_target_right_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->target_right_open_gap_score = score;
    self->target_right_extend_gap_score = score;
    if (self->target_gap_function) {
        Py_DECREF(self->target_gap_function);
        self->target_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_end_gap_score__doc__[] = "query end score";

static PyObject*
Aligner_get_query_end_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_left_open_gap_score;
        if (score != self->query_left_extend_gap_score
         || score != self->query_right_open_gap_score
         || score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_end_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_open_gap_score = score;
    self->query_left_extend_gap_score = score;
    self->query_right_open_gap_score = score;
    self->query_right_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_end_open_gap_score__doc__[] = "query end open score";

static PyObject*
Aligner_get_query_end_open_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_left_open_gap_score;
        if (score != self->query_right_open_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_end_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_open_gap_score = score;
    self->query_right_open_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_end_extend_gap_score__doc__[] = "query end extend score";

static PyObject*
Aligner_get_query_end_extend_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_left_extend_gap_score;
        if (score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_end_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_extend_gap_score = score;
    self->query_right_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_internal_open_gap_score__doc__[] = "query internal open gap score";

static PyObject*
Aligner_get_query_internal_open_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_open_gap_score);
}

static int
Aligner_set_query_internal_open_gap_score(Aligner* self, PyObject* value,
                                          void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_open_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_internal_extend_gap_score__doc__[] = "query internal extend gap score";

static PyObject*
Aligner_get_query_internal_extend_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_extend_gap_score);
}

static int
Aligner_set_query_internal_extend_gap_score(Aligner* self, PyObject* value,
                                            void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_internal_gap_score__doc__[] = "query internal gap score";

static PyObject*
Aligner_get_query_internal_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_open_gap_score;
        if (score != self->query_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_internal_gap_score(Aligner* self, PyObject* value,
                                     void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_open_gap_score = score;
    self->query_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_left_open_gap_score__doc__[] = "query left open score";

static PyObject*
Aligner_get_query_left_open_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_left_open_gap_score);
}

static int
Aligner_set_query_left_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_open_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_left_extend_gap_score__doc__[] = "query left extend score";

static PyObject*
Aligner_get_query_left_extend_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_left_extend_gap_score);
}

static int
Aligner_set_query_left_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_left_gap_score__doc__[] = "query left score";

static PyObject*
Aligner_get_query_left_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_left_open_gap_score;
        if (score != self->query_left_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_left_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_left_open_gap_score = score;
    self->query_left_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_right_open_gap_score__doc__[] = "query right open score";

static PyObject*
Aligner_get_query_right_open_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_right_open_gap_score);
}

static int
Aligner_set_query_right_open_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_right_open_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_right_extend_gap_score__doc__[] = "query right extend score";

static PyObject*
Aligner_get_query_right_extend_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    return PyFloat_FromDouble(self->query_right_extend_gap_score);
}

static int
Aligner_set_query_right_extend_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_right_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_query_right_gap_score__doc__[] = "query right score";

static PyObject*
Aligner_get_query_right_gap_score(Aligner* self, void* closure)
{   if (self->query_gap_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->query_right_open_gap_score;
        if (score != self->query_right_extend_gap_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_query_right_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->query_right_open_gap_score = score;
    self->query_right_extend_gap_score = score;
    if (self->query_gap_function) {
        Py_DECREF(self->query_gap_function);
        self->query_gap_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_epsilon__doc__[] = "roundoff epsilon";

static PyObject*
Aligner_get_epsilon(Aligner* self, void* closure)
{   return PyFloat_FromDouble(self->epsilon);
}

static int
Aligner_set_epsilon(Aligner* self, PyObject* value, void* closure)
{   const double epsilon = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->epsilon = epsilon;
    self->algorithm = Unknown;
    return 0;
}

static Algorithm _get_algorithm(Aligner* self)
{
    Algorithm algorithm = self->algorithm;
    if (algorithm == Unknown) {
        const double target_gap_open = self->target_open_gap_score;
        const double query_gap_open = self->query_open_gap_score;
        const double target_gap_extend = self->target_extend_gap_score;
        const double query_gap_extend = self->query_extend_gap_score;
        const double target_left_open = self->target_left_open_gap_score;
        const double target_left_extend = self->target_left_extend_gap_score;
        const double query_left_open = self->query_left_open_gap_score;
        const double target_right_open = self->target_right_open_gap_score;
        const double query_right_open = self->query_right_open_gap_score;
        const double target_right_extend = self->target_right_extend_gap_score;
        const double query_left_extend = self->query_left_extend_gap_score;
        const double query_right_extend = self->query_right_extend_gap_score;
        if (self->target_gap_function || self->query_gap_function)
            algorithm = WatermanSmithBeyer;
        else if (target_gap_open == target_gap_extend
              && query_gap_open == query_gap_extend
              && target_left_open == target_left_extend
              && target_right_open == target_right_extend
              && query_left_open == query_left_extend
              && query_right_open == query_right_extend)
            algorithm = NeedlemanWunschSmithWaterman;
        else
            algorithm = Gotoh;
        self->algorithm = algorithm;
    }
    return algorithm;
}


static char Aligner_algorithm__doc__[] = "alignment algorithm";

static PyObject*
Aligner_get_algorithm(Aligner* self, void* closure)
{
    const char* s = NULL;
    const Mode mode = self->mode;
    const Algorithm algorithm = _get_algorithm(self);
    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    s = "Needleman-Wunsch";
                    break;
                case Local:
                    s = "Smith-Waterman";
                    break;
            }
            break;
        case Gotoh:
            switch (mode) {
                case Global:
                    s = "Gotoh global alignment algorithm";
                    break;
                case Local:
                    s = "Gotoh local alignment algorithm";
                    break;
            }
            break;
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    s = "Waterman-Smith-Beyer global alignment algorithm";
                    break;
                case Local:
                    s = "Waterman-Smith-Beyer local alignment algorithm";
                    break;
            }
            break;
        case Unknown:
        default:
            break;
    }
#if PY_MAJOR_VERSION >= 3
    return PyUnicode_FromString(s);
#else
    return PyString_FromString(s);
#endif
}

static PyGetSetDef Aligner_getset[] = {
    {"mode",
        (getter)Aligner_get_mode,
        (setter)Aligner_set_mode,
        Aligner_mode__doc__, NULL},
    {"match",
        (getter)Aligner_get_match,
        (setter)Aligner_set_match,
        Aligner_match__doc__, NULL},
    {"mismatch",
        (getter)Aligner_get_mismatch,
        (setter)Aligner_set_mismatch,
        Aligner_mismatch__doc__, NULL},
    {"substitution_matrix",
        (getter)Aligner_get_substitution_matrix,
        (setter)Aligner_set_substitution_matrix,
        Aligner_substitution_matrix__doc__, NULL},
    {"gap_score",
        (getter)Aligner_get_gap_score,
        (setter)Aligner_set_gap_score,
        Aligner_gap_score__doc__, NULL},
    {"open_gap_score",
        (getter)Aligner_get_open_gap_score,
        (setter)Aligner_set_open_gap_score,
        Aligner_open_gap_score__doc__, NULL},
    {"extend_gap_score",
        (getter)Aligner_get_extend_gap_score,
        (setter)Aligner_set_extend_gap_score,
        Aligner_extend_gap_score__doc__, NULL},
    {"internal_gap_score",
        (getter)Aligner_get_internal_gap_score,
        (setter)Aligner_set_internal_gap_score,
        Aligner_internal_gap_score__doc__, NULL},
    {"internal_open_gap_score",
        (getter)Aligner_get_internal_open_gap_score,
        (setter)Aligner_set_internal_open_gap_score,
        Aligner_internal_open_gap_score__doc__, NULL},
    {"internal_extend_gap_score",
        (getter)Aligner_get_internal_extend_gap_score,
        (setter)Aligner_set_internal_extend_gap_score,
        Aligner_internal_extend_gap_score__doc__, NULL},
    {"end_gap_score",
        (getter)Aligner_get_end_gap_score,
        (setter)Aligner_set_end_gap_score,
        Aligner_end_gap_score__doc__, NULL},
    {"end_open_gap_score",
        (getter)Aligner_get_end_open_gap_score,
        (setter)Aligner_set_end_open_gap_score,
        Aligner_end_open_gap_score__doc__, NULL},
    {"end_extend_gap_score",
        (getter)Aligner_get_end_extend_gap_score,
        (setter)Aligner_set_end_extend_gap_score,
        Aligner_end_extend_gap_score__doc__, NULL},
    {"left_gap_score",
        (getter)Aligner_get_left_gap_score,
        (setter)Aligner_set_left_gap_score,
        Aligner_left_gap_score__doc__, NULL},
    {"left_open_gap_score",
        (getter)Aligner_get_left_open_gap_score,
        (setter)Aligner_set_left_open_gap_score,
        Aligner_left_open_gap_score__doc__, NULL},
    {"left_extend_gap_score",
        (getter)Aligner_get_left_extend_gap_score,
        (setter)Aligner_set_left_extend_gap_score,
        Aligner_left_extend_gap_score__doc__, NULL},
    {"right_gap_score",
        (getter)Aligner_get_right_gap_score,
        (setter)Aligner_set_right_gap_score,
        Aligner_right_gap_score__doc__, NULL},
    {"right_open_gap_score",
        (getter)Aligner_get_right_open_gap_score,
        (setter)Aligner_set_right_open_gap_score,
        Aligner_right_open_gap_score__doc__, NULL},
    {"right_extend_gap_score",
        (getter)Aligner_get_right_extend_gap_score,
        (setter)Aligner_set_right_extend_gap_score,
        Aligner_right_extend_gap_score__doc__, NULL},
    {"target_open_gap_score",
        (getter)Aligner_get_target_open_gap_score,
        (setter)Aligner_set_target_open_gap_score,
        Aligner_target_open_gap_score__doc__, NULL},
    {"target_extend_gap_score",
        (getter)Aligner_get_target_extend_gap_score,
        (setter)Aligner_set_target_extend_gap_score,
        Aligner_target_extend_gap_score__doc__, NULL},
    {"target_gap_score",
        (getter)Aligner_get_target_gap_score,
        (setter)Aligner_set_target_gap_score,
        Aligner_target_gap_score__doc__, NULL},
    {"query_open_gap_score",
        (getter)Aligner_get_query_open_gap_score,
        (setter)Aligner_set_query_open_gap_score,
        Aligner_query_open_gap_score__doc__, NULL},
    {"query_extend_gap_score",
        (getter)Aligner_get_query_extend_gap_score,
        (setter)Aligner_set_query_extend_gap_score,
        Aligner_query_extend_gap_score__doc__, NULL},
    {"query_gap_score",
        (getter)Aligner_get_query_gap_score,
        (setter)Aligner_set_query_gap_score,
        Aligner_query_gap_score__doc__, NULL},
    {"target_end_gap_score",
        (getter)Aligner_get_target_end_gap_score,
        (setter)Aligner_set_target_end_gap_score,
        Aligner_target_end_gap_score__doc__, NULL},
    {"target_end_open_gap_score",
        (getter)Aligner_get_target_end_open_gap_score,
        (setter)Aligner_set_target_end_open_gap_score,
        Aligner_target_end_open_gap_score__doc__, NULL},
    {"target_end_extend_gap_score",
        (getter)Aligner_get_target_end_extend_gap_score,
        (setter)Aligner_set_target_end_extend_gap_score,
        Aligner_target_end_extend_gap_score__doc__, NULL},
    {"target_internal_open_gap_score",
        (getter)Aligner_get_target_internal_open_gap_score,
        (setter)Aligner_set_target_internal_open_gap_score,
        Aligner_target_internal_open_gap_score__doc__, NULL},
    {"target_internal_extend_gap_score",
        (getter)Aligner_get_target_internal_extend_gap_score,
        (setter)Aligner_set_target_internal_extend_gap_score,
        Aligner_target_internal_extend_gap_score__doc__, NULL},
    {"target_internal_gap_score",
        (getter)Aligner_get_target_internal_gap_score,
        (setter)Aligner_set_target_internal_gap_score,
        Aligner_target_internal_gap_score__doc__, NULL},
    {"target_left_open_gap_score",
        (getter)Aligner_get_target_left_open_gap_score,
        (setter)Aligner_set_target_left_open_gap_score,
        Aligner_target_left_open_gap_score__doc__, NULL},
    {"target_left_extend_gap_score",
        (getter)Aligner_get_target_left_extend_gap_score,
        (setter)Aligner_set_target_left_extend_gap_score,
        Aligner_target_left_extend_gap_score__doc__, NULL},
    {"target_left_gap_score",
        (getter)Aligner_get_target_left_gap_score,
        (setter)Aligner_set_target_left_gap_score,
        Aligner_target_left_gap_score__doc__, NULL},
    {"target_right_open_gap_score",
        (getter)Aligner_get_target_right_open_gap_score,
        (setter)Aligner_set_target_right_open_gap_score,
        Aligner_target_right_gap_score_open__doc__, NULL},
    {"target_right_extend_gap_score",
        (getter)Aligner_get_target_right_extend_gap_score,
        (setter)Aligner_set_target_right_extend_gap_score,
        Aligner_target_right_extend_gap_score__doc__, NULL},
    {"target_right_gap_score",
        (getter)Aligner_get_target_right_gap_score,
        (setter)Aligner_set_target_right_gap_score,
        Aligner_target_right_gap_score__doc__, NULL},
    {"query_end_gap_score",
        (getter)Aligner_get_query_end_gap_score,
        (setter)Aligner_set_query_end_gap_score,
        Aligner_query_end_gap_score__doc__, NULL},
    {"query_end_open_gap_score",
        (getter)Aligner_get_query_end_open_gap_score,
        (setter)Aligner_set_query_end_open_gap_score,
        Aligner_query_end_open_gap_score__doc__, NULL},
    {"query_end_extend_gap_score",
        (getter)Aligner_get_query_end_extend_gap_score,
        (setter)Aligner_set_query_end_extend_gap_score,
        Aligner_query_end_extend_gap_score__doc__, NULL},
    {"query_internal_open_gap_score",
        (getter)Aligner_get_query_internal_open_gap_score,
        (setter)Aligner_set_query_internal_open_gap_score,
        Aligner_query_internal_open_gap_score__doc__, NULL},
    {"query_internal_extend_gap_score",
        (getter)Aligner_get_query_internal_extend_gap_score,
        (setter)Aligner_set_query_internal_extend_gap_score,
        Aligner_query_internal_extend_gap_score__doc__, NULL},
    {"query_internal_gap_score",
        (getter)Aligner_get_query_internal_gap_score,
        (setter)Aligner_set_query_internal_gap_score,
        Aligner_query_internal_gap_score__doc__, NULL},
    {"query_left_open_gap_score",
        (getter)Aligner_get_query_left_open_gap_score,
        (setter)Aligner_set_query_left_open_gap_score,
        Aligner_query_left_open_gap_score__doc__, NULL},
    {"query_left_extend_gap_score",
        (getter)Aligner_get_query_left_extend_gap_score,
        (setter)Aligner_set_query_left_extend_gap_score,
        Aligner_query_left_extend_gap_score__doc__, NULL},
    {"query_left_gap_score",
        (getter)Aligner_get_query_left_gap_score,
        (setter)Aligner_set_query_left_gap_score,
         Aligner_query_left_gap_score__doc__, NULL},
    {"query_right_open_gap_score",
        (getter)Aligner_get_query_right_open_gap_score,
        (setter)Aligner_set_query_right_open_gap_score,
        Aligner_query_right_open_gap_score__doc__, NULL},
    {"query_right_extend_gap_score",
        (getter)Aligner_get_query_right_extend_gap_score,
        (setter)Aligner_set_query_right_extend_gap_score,
        Aligner_query_right_extend_gap_score__doc__, NULL},
    {"query_right_gap_score",
        (getter)Aligner_get_query_right_gap_score,
        (setter)Aligner_set_query_right_gap_score,
        Aligner_query_right_gap_score__doc__, NULL},
    {"epsilon",
        (getter)Aligner_get_epsilon,
        (setter)Aligner_set_epsilon,
        Aligner_epsilon__doc__, NULL},
    {"algorithm",
        (getter)Aligner_get_algorithm,
        (setter)NULL,
        Aligner_algorithm__doc__, NULL},
    {NULL}  /* Sentinel */
};

#define SELECT_SCORE_GLOBAL(score1, score2, score3) \
    score = score1; \
    temp = score2; \
    if (temp > score) score = temp; \
    temp = score3; \
    if (temp > score) score = temp;

#define SELECT_SCORE_WATERMAN_SMITH_BEYER(score1, score2) \
    temp = score1 + gapscore; \
    if (temp > score) score = temp; \
    temp = score2 + gapscore; \
    if (temp > score) score = temp;

#define SELECT_SCORE_GOTOH_LOCAL_ALIGN(score1, score2, score3, score4) \
    score = score1; \
    temp = score2; \
    if (temp > score) score = temp; \
    temp = score3; \
    if (temp > score) score = temp; \
    score += score4; \
    if (score < 0) score = 0; \
    else if (score > maximum) maximum = score;

#define SELECT_SCORE_LOCAL3(score1, score2, score3) \
    score = score1; \
    temp = score2; \
    if (temp > score) score = temp; \
    temp = score3; \
    if (temp > score) score = temp; \
    if (score < 0) score = 0; \
    else if (score > maximum) maximum = score;

#define SELECT_SCORE_LOCAL1(score1) \
    score = score1; \
    if (score < 0) score = 0; \
    else if (score > maximum) maximum = score;

#define SELECT_TRACE_NEEDLEMAN_WUNSCH(hgap, vgap) \
    score = temp + self->substitution_matrix[kA][kB]; \
    trace = DIAGONAL; \
    temp = scores[j-1] + hgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = HORIZONTAL; \
    } \
    else if (temp > score - epsilon) trace |= HORIZONTAL; \
    temp = scores[j] + vgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = VERTICAL; \
    } \
    else if (temp > score - epsilon) trace |= VERTICAL; \
    temp = scores[j]; \
    scores[j] = score; \
    M[i][j].trace = trace;

#define SELECT_TRACE_SMITH_WATERMAN_HVD(hgap, vgap) \
    trace = DIAGONAL; \
    score = temp + self->substitution_matrix[kA][kB]; \
    temp = scores[j-1] + hgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = HORIZONTAL; \
    } \
    else if (temp > score - epsilon) trace |= HORIZONTAL; \
    temp = scores[j] + vgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = VERTICAL; \
    } \
    else if (temp > score - epsilon) trace |= VERTICAL; \
    if (score < epsilon) { \
        score = 0; \
        trace = 0; \
    } \
    else if (trace & DIAGONAL && score > maximum - epsilon) { \
        if (score > maximum + epsilon) { \
            for ( ; im < i; im++, jm = 0) \
                for ( ; jm <= nB; jm++) M[im][jm].trace &= ~ENDPOINT; \
            for ( ; jm < j; jm++) M[im][jm].trace &= ~ENDPOINT; \
            im = i; \
            jm = j; \
        } \
        trace |= ENDPOINT; \
    } \
    M[i][j].trace = trace; \
    if (score > maximum) maximum = score; \
    temp = scores[j]; \
    scores[j] = score;

#define SELECT_TRACE_SMITH_WATERMAN_D \
    score = temp + self->substitution_matrix[kA][kB]; \
    trace = DIAGONAL; \
    if (score < epsilon) { \
        score = 0; \
        trace = 0; \
    } \
    else if (trace & DIAGONAL && score > maximum - epsilon) { \
        if (score > maximum + epsilon) { \
            for ( ; im < i; im++, jm = 0) \
                for ( ; jm <= nB; jm++) M[im][jm].trace &= ~ENDPOINT; \
            for ( ; jm < j; jm++) M[im][jm].trace &= ~ENDPOINT; \
            im = i; \
            jm = j; \
        } \
        trace |= ENDPOINT; \
    } \
    M[i][j].trace = trace; \
    if (score > maximum) maximum = score; \
    temp = scores[j]; \
    scores[j] = score

#define SELECT_TRACE_GOTOH_GLOBAL_GAP(matrix, score1, score2, score3) \
    trace = M_MATRIX; \
    score = score1; \
    temp = score2; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Ix_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Ix_MATRIX; \
    temp = score3; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Iy_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Iy_MATRIX; \
    gaps[i][j].matrix = trace;

#define SELECT_TRACE_GOTOH_GLOBAL_ALIGN \
    trace = M_MATRIX; \
    score = M_temp; \
    temp = Ix_temp; \
    if (temp > score + epsilon) { \
        score = Ix_temp; \
        trace = Ix_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Ix_MATRIX; \
    temp = Iy_temp; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Iy_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Iy_MATRIX; \
    M[i][j].trace = trace;

#define SELECT_TRACE_GOTOH_LOCAL_ALIGN \
    trace = M_MATRIX; \
    score = M_temp; \
    if (Ix_temp > score + epsilon) { \
        score = Ix_temp; \
        trace = Ix_MATRIX; \
    } \
    else if (Ix_temp > score - epsilon) trace |= Ix_MATRIX; \
    if (Iy_temp > score + epsilon) { \
        score = Iy_temp; \
        trace = Iy_MATRIX; \
    } \
    else if (Iy_temp > score - epsilon) trace |= Iy_MATRIX; \
    score += self->substitution_matrix[kA][kB]; \
    if (score < epsilon) { \
        score = 0; \
        trace = 0; \
    } \
    else if (score > maximum - epsilon) { \
        if (score > maximum + epsilon) { \
            maximum = score; \
            for ( ; im < i; im++, jm = 0) \
                for ( ; jm <= nB; jm++) M[im][jm].trace &= ~ENDPOINT; \
            for ( ; jm < j; jm++) M[im][jm].trace &= ~ENDPOINT; \
            im = i; \
            jm = j; \
        } \
        trace |= ENDPOINT; \
    } \
    M[i][j].trace = trace;

#define SELECT_TRACE_GOTOH_LOCAL_GAP(matrix, score1, score2, score3) \
    trace = M_MATRIX; \
    score = score1; \
    temp = score2; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Ix_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Ix_MATRIX; \
    temp = score3; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Iy_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Iy_MATRIX; \
    if (score < epsilon) { \
        score = -DBL_MAX; \
        trace = 0; \
    } \
    gaps[i][j].matrix = trace;

#define SELECT_TRACE_WATERMAN_SMITH_BEYER_GLOBAL_ALIGN(score4) \
    trace = M_MATRIX; \
    score = M_scores[i-1][j-1]; \
    temp = Ix_scores[i-1][j-1]; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Ix_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Ix_MATRIX; \
    temp = Iy_scores[i-1][j-1]; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Iy_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Iy_MATRIX; \
    M_scores[i][j] = score + score4; \
    M[i][j].trace = trace;

#define SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(score1, score2) \
    temp = score1 + gapscore; \
    if (temp > score - epsilon) { \
        if (temp > score + epsilon) { \
            score = temp; \
            nm = 0; \
            ng = 0; \
        } \
        gapM[nm] = gap; \
        nm++; \
    } \
    temp = score2 + gapscore; \
    if (temp > score - epsilon) { \
        if (temp > score + epsilon) { \
            score = temp; \
            nm = 0; \
            ng = 0; \
        } \
        gapXY[ng] = gap; \
        ng++; \
    }

#define SELECT_TRACE_WATERMAN_SMITH_BEYER_ALIGN(score1, score2, score3, score4) \
    trace = M_MATRIX; \
    score = score1; \
    if (score2 > score + epsilon) { \
        score = score2; \
        trace = Ix_MATRIX; \
    } \
    else if (score2 > score - epsilon) trace |= Ix_MATRIX; \
    if (score3 > score + epsilon) { \
        score = score3; \
        trace = Iy_MATRIX; \
    } \
    else if (score3 > score - epsilon) trace |= Iy_MATRIX; \
    score += score4; \
    if (score < epsilon) { \
        score = 0; \
        trace = 0; \
    } \
    else if (score > maximum - epsilon) { \
        if (score > maximum + epsilon) { \
            maximum = score; \
            for ( ; im < i; im++, jm = 0) \
                for ( ; jm <= nB; jm++) M[im][jm].trace &= ~ENDPOINT; \
            for ( ; jm < j; jm++) M[im][jm].trace &= ~ENDPOINT; \
            im = i; \
            jm = j; \
        } \
        trace |= ENDPOINT; \
    } \
    M_scores[i][j] = score; \
    M[i][j].trace = trace;

/* -------------- allocation & deallocation ------------- */

static PathGenerator*
PathGenerator_create_NWSW(Py_ssize_t nA, Py_ssize_t nB, Mode mode)
{
    int i;
    unsigned char trace = 0;
    Trace** M;
    PathGenerator* paths;

    paths = (PathGenerator*)PyType_GenericAlloc(&PathGenerator_Type, 0);
    if (!paths) return NULL;

    paths->iA = 0;
    paths->iB = 0;
    paths->nA = nA;
    paths->nB = nB;
    paths->M = NULL;
    paths->gaps.gotoh = NULL;
    paths->gaps.waterman_smith_beyer = NULL;
    paths->algorithm = NeedlemanWunschSmithWaterman;
    paths->mode = mode;
    paths->length = 0;

    M = PyMem_Malloc((nA+1)*sizeof(Trace*));
    paths->M = M;
    if (!M) goto exit;
    if (mode == Global) trace = VERTICAL;
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(Trace));
        if (!M[i]) goto exit;
        M[i][0].trace = trace;
    }
    M[0][0].trace = 0;
    if (mode == Global) trace = HORIZONTAL;
    for (i = 1; i <= nB; i++) M[0][i].trace = trace;
    M[0][0].path = 0;
    return paths;
exit:
    Py_DECREF(paths);
    PyErr_SetNone(PyExc_MemoryError);
    return NULL;
}

static PathGenerator*
PathGenerator_create_Gotoh(Py_ssize_t nA, Py_ssize_t nB, Mode mode)
{
    int i;
    Trace** M;
    TraceGapsGotoh** gaps;
    PathGenerator* paths;

    paths = (PathGenerator*)PyType_GenericAlloc(&PathGenerator_Type, 0);
    if (!paths) return NULL;

    paths->iA = 0;
    paths->iB = 0;
    paths->nA = nA;
    paths->nB = nB;
    paths->M = NULL;
    paths->gaps.gotoh = NULL;
    paths->algorithm = Gotoh;
    paths->mode = mode;
    paths->length = 0;

    M = PyMem_Malloc((nA+1)*sizeof(Trace*));
    if (!M) goto exit;
    paths->M = M;
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(Trace));
        if (!M[i]) goto exit;
        M[i][0].trace = 0;
    }
    gaps = PyMem_Malloc((nA+1)*sizeof(TraceGapsGotoh*));
    if (!gaps) goto exit;
    paths->gaps.gotoh = gaps;
    for (i = 0; i <= nA; i++) {
        gaps[i] = PyMem_Malloc((nB+1)*sizeof(TraceGapsGotoh));
        if (!gaps[i]) goto exit;
    }

    gaps[0][0].Ix = 0;
    gaps[0][0].Iy = 0;
    if (mode == Global) {
        for (i = 1; i <= nA; i++) {
            gaps[i][0].Ix = Ix_MATRIX;
            gaps[i][0].Iy = 0;
        }
        gaps[1][0].Ix = M_MATRIX;
        for (i = 1; i <= nB; i++) {
            M[0][i].trace = 0;
            gaps[0][i].Ix = 0;
            gaps[0][i].Iy = Iy_MATRIX;
        }
        gaps[0][1].Iy = M_MATRIX;
    }
    else if (mode == Local) {
        for (i = 1; i < nA; i++) {
            gaps[i][0].Ix = 0;
            gaps[i][0].Iy = 0;
        }
        for (i = 1; i <= nB; i++) {
            M[0][i].trace = 0;
            gaps[0][i].Ix = 0;
            gaps[0][i].Iy = 0;
        }
    }
    M[0][0].path = 0;

    return paths;
exit:
    Py_DECREF(paths);
    PyErr_SetNone(PyExc_MemoryError);
    return NULL;
}

static PathGenerator*
PathGenerator_create_WSB(Py_ssize_t nA, Py_ssize_t nB, Mode mode)
{
    int i, j;
    int* trace;
    Trace** M = NULL;
    TraceGapsWatermanSmithBeyer** gaps = NULL;
    PathGenerator* paths;

    paths = (PathGenerator*)PyType_GenericAlloc(&PathGenerator_Type, 0);
    if (!paths) return NULL;

    paths->iA = 0;
    paths->iB = 0;
    paths->nA = nA;
    paths->nB = nB;
    paths->M = NULL;
    paths->gaps.waterman_smith_beyer = NULL;
    paths->algorithm = WatermanSmithBeyer;
    paths->mode = mode;
    paths->length = 0;

    M = PyMem_Malloc((nA+1)*sizeof(Trace*));
    if (!M) goto exit;
    paths->M = M;
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(Trace));
        if (!M[i]) goto exit;
    }
    gaps = PyMem_Malloc((nA+1)*sizeof(TraceGapsWatermanSmithBeyer*));
    if (!gaps) goto exit;
    paths->gaps.waterman_smith_beyer = gaps;
    for (i = 0; i <= nA; i++) gaps[i] = NULL;
    for (i = 0; i <= nA; i++) {
        gaps[i] = PyMem_Malloc((nB+1)*sizeof(TraceGapsWatermanSmithBeyer));
        if (!gaps[i]) goto exit;
        for (j = 0; j <= nB; j++) {
            gaps[i][j].MIx = NULL;
            gaps[i][j].IyIx = NULL;
            gaps[i][j].MIy = NULL;
            gaps[i][j].IxIy = NULL;
        }
        M[i][0].trace = 0;
        M[i][0].path = 0;
        if (mode == Global) {
            trace = PyMem_Malloc(2*sizeof(int));
            if (!trace) goto exit;
            gaps[i][0].MIx = trace;
            trace[0] = i;
            trace[1] = 0;
            trace = PyMem_Malloc(sizeof(int));
            if (!trace) goto exit;
            gaps[i][0].IyIx = trace;
            trace[0] = 0;
        }
    }
    for (i = 1; i <= nB; i++) {
        M[0][i].trace = 0;
        if (mode == Global) {
            trace = PyMem_Malloc(2*sizeof(int));
            if (!trace) goto exit;
            gaps[0][i].MIy = trace;
            trace[0] = i;
            trace[1] = 0;
            trace = PyMem_Malloc(sizeof(int));
            if (!trace) goto exit;
            gaps[0][i].IxIy = trace;
            trace[0] = 0;
        }
    }
    M[0][0].path = 0;
    return paths;
exit:
    Py_DECREF(paths);
    PyErr_SetNone(PyExc_MemoryError);
    return NULL;
}

/* ----------------- alignment algorithms ----------------- */

static PyObject*
Aligner_needlemanwunsch_score(Aligner* self, const char* sA, Py_ssize_t nA,
                                             const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double left_gap_extend_A = self->target_left_extend_gap_score;
    const double right_gap_extend_A = self->target_right_extend_gap_score;
    const double left_gap_extend_B = self->query_left_extend_gap_score;
    const double right_gap_extend_B = self->query_right_extend_gap_score;
    double score;
    double temp;

    double* scores;

    /* Needleman-Wunsch algorithm */
    scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!scores) return PyErr_NoMemory();

    /* The top row of the score matrix is a special case,
     * as there are no previously aligned characters.
     */
    scores[0] = 0.0;
    for (j = 1; j <= nB; j++) scores[j] = j * left_gap_extend_A;
    for (i = 1; i < nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        temp = scores[0];
        scores[0] = i * left_gap_extend_B;
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_GLOBAL(temp + self->substitution_matrix[kA][kB],
                                scores[j] + gap_extend_B,
                                scores[j-1] + gap_extend_A);
            temp = scores[j];
            scores[j] = score;
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_SCORE_GLOBAL(temp + self->substitution_matrix[kA][kB],
                            scores[nB] + right_gap_extend_B,
                            scores[nB-1] + gap_extend_A);
        temp = scores[nB];
        scores[nB] = score;
    }
    kA = CHARINDEX(sA[nA-1]);
    temp = scores[0];
    scores[0] = nA * right_gap_extend_B;
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_SCORE_GLOBAL(temp + self->substitution_matrix[kA][kB],
                            scores[j] + gap_extend_B,
                            scores[j-1] + right_gap_extend_A);
        temp = scores[j];
        scores[j] = score;
    }
    kB = CHARINDEX(sB[nB-1]);
    SELECT_SCORE_GLOBAL(temp + self->substitution_matrix[kA][kB],
                        scores[nB] + right_gap_extend_B,
                        scores[nB-1] + right_gap_extend_A);
    PyMem_Free(scores);
    return PyFloat_FromDouble(score);
}

static PyObject*
Aligner_smithwaterman_score(Aligner* self, const char* sA, Py_ssize_t nA,
                                           const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    double score;
    double* scores;
    double temp;
    double maximum = 0;

    /* Smith-Waterman algorithm */
    scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!scores) return PyErr_NoMemory();

    /* The top row of the score matrix is a special case,
     * as there are no previously aligned characters.
     */
    for (j = 0; j <= nB; j++)
        scores[j] = 0;
    for (i = 1; i < nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        temp = 0;
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_LOCAL3(temp + self->substitution_matrix[kA][kB],
                                scores[j] + gap_extend_B,
                                scores[j-1] + gap_extend_A);
            temp = scores[j];
            scores[j] = score;
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_SCORE_LOCAL1(temp + self->substitution_matrix[kA][kB]);
        temp = scores[nB];
        scores[nB] = score;
    }
    kA = CHARINDEX(sA[nA-1]);
    temp = 0;
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_SCORE_LOCAL1(temp + self->substitution_matrix[kA][kB]);
        temp = scores[j];
        scores[j] = score;
    }
    kB = CHARINDEX(sB[nB-1]);
    SELECT_SCORE_LOCAL1(temp + self->substitution_matrix[kA][kB]);
    PyMem_Free(scores);
    return PyFloat_FromDouble(maximum);
}

static PyObject*
Aligner_needlemanwunsch_align(Aligner* self, const char* sA, Py_ssize_t nA,
                                             const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double left_gap_extend_A = self->target_left_extend_gap_score;
    const double left_gap_extend_B = self->query_left_extend_gap_score;
    const double right_gap_extend_A = self->target_right_extend_gap_score;
    const double right_gap_extend_B = self->query_right_extend_gap_score;
    const double epsilon = self->epsilon;
    Trace** M;
    double score;
    int trace;
    double temp;
    double* scores = NULL;
    PathGenerator* paths;

    /* Needleman-Wunsch algorithm */
    paths = PathGenerator_create_NWSW(nA, nB, Global);
    if (!paths) return NULL;
    scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!scores) {
        Py_DECREF(paths);
        return PyErr_NoMemory();
    }
    M = paths->M;
    scores[0] = 0;
    for (j = 1; j <= nB; j++) scores[j] = j * left_gap_extend_A;
    for (i = 1; i < nA; i++) {
        temp = scores[0];
        scores[0] = i * left_gap_extend_B;
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_TRACE_NEEDLEMAN_WUNSCH(gap_extend_A, gap_extend_B);
        }
        kB = CHARINDEX(sB[j-1]);
        SELECT_TRACE_NEEDLEMAN_WUNSCH(gap_extend_A, right_gap_extend_B);
    }
    temp = scores[0];
    scores[0] = i * left_gap_extend_B;
    kA = CHARINDEX(sA[nA-1]);
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_TRACE_NEEDLEMAN_WUNSCH(right_gap_extend_A, gap_extend_B);
    }
    kB = CHARINDEX(sB[j-1]);
    SELECT_TRACE_NEEDLEMAN_WUNSCH(right_gap_extend_A, right_gap_extend_B);
    PyMem_Free(scores);
    M[nA][nB].path = 0;

    return Py_BuildValue("fN", score, paths);
}

static PyObject*
Aligner_smithwaterman_align(Aligner* self, const char* sA, Py_ssize_t nA,
                                           const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int im = nA;
    int jm = nB;
    int kA;
    int kB;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double epsilon = self->epsilon;
    Trace** M = NULL;
    double maximum = 0;
    double score = 0;
    double* scores = NULL;
    double temp;
    int trace;
    PathGenerator* paths = NULL;

    /* Smith-Waterman algorithm */
    paths = PathGenerator_create_NWSW(nA, nB, Local);
    if (!paths) return NULL;
    scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!scores) {
        Py_DECREF(paths);
        return PyErr_NoMemory();
    }
    M = paths->M;
    for (j = 0; j <= nB; j++) scores[j] = 0;
    for (i = 1; i < nA; i++) {
        temp = 0;
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_TRACE_SMITH_WATERMAN_HVD(gap_extend_A, gap_extend_B);
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_TRACE_SMITH_WATERMAN_D;
    }
    temp = 0;
    kA = CHARINDEX(sA[nA-1]);
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_TRACE_SMITH_WATERMAN_D;
    }
    kB = CHARINDEX(sB[nB-1]);
    SELECT_TRACE_SMITH_WATERMAN_D;
    PyMem_Free(scores);

    if (maximum == 0) M[0][0].path = NONE;
    else M[0][0].path = 0;

    return Py_BuildValue("fN", maximum, paths);
}

static PyObject*
Aligner_gotoh_global_score(Aligner* self, const char* sA, Py_ssize_t nA,
                                          const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_open_A = self->target_open_gap_score;
    const double gap_open_B = self->query_open_gap_score;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double left_gap_open_A = self->target_left_open_gap_score;
    const double left_gap_open_B = self->query_left_open_gap_score;
    const double left_gap_extend_A = self->target_left_extend_gap_score;
    const double left_gap_extend_B = self->query_left_extend_gap_score;
    const double right_gap_open_A = self->target_right_open_gap_score;
    const double right_gap_open_B = self->query_right_open_gap_score;
    const double right_gap_extend_A = self->target_right_extend_gap_score;
    const double right_gap_extend_B = self->query_right_extend_gap_score;
    double* M_scores = NULL;
    double* Ix_scores = NULL;
    double* Iy_scores = NULL;
    double score;
    double temp;
    double M_temp;
    double Ix_temp;
    double Iy_temp;

    /* Gotoh algorithm with three states */
    M_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Iy_scores) goto exit;

    /* The top row of the score matrix is a special case,
     * as there are no previously aligned characters.
     */
    M_scores[0] = 0;
    Ix_scores[0] = -DBL_MAX;
    Iy_scores[0] = -DBL_MAX;
    for (j = 1; j <= nB; j++) {
        M_scores[j] = -DBL_MAX;
        Ix_scores[j] = -DBL_MAX;
        Iy_scores[j] = left_gap_open_A + left_gap_extend_A * (j-1);
    }

    for (i = 1; i < nA; i++) {
        M_temp = M_scores[0];
        Ix_temp = Ix_scores[0];
        Iy_temp = Iy_scores[0];
        M_scores[0] = -DBL_MAX;
        Ix_scores[0] = left_gap_open_B + left_gap_extend_B * (i-1);
        Iy_scores[0] = -DBL_MAX;
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_GLOBAL(M_temp,
                                Ix_temp,
                                Iy_temp);
            M_temp = M_scores[j];
            M_scores[j] = score + self->substitution_matrix[kA][kB];
            SELECT_SCORE_GLOBAL(M_temp + gap_open_B,
                                Ix_scores[j] + gap_extend_B,
                                Iy_scores[j] + gap_open_B);
            Ix_temp = Ix_scores[j];
            Ix_scores[j] = score;
            SELECT_SCORE_GLOBAL(M_scores[j-1] + gap_open_A,
                                Ix_scores[j-1] + gap_open_A,
                                Iy_scores[j-1] + gap_extend_A);
            Iy_temp = Iy_scores[j];
            Iy_scores[j] = score;
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_SCORE_GLOBAL(M_temp,
                            Ix_temp,
                            Iy_temp);
        M_temp = M_scores[nB];
        M_scores[nB] = score + self->substitution_matrix[kA][kB];
        SELECT_SCORE_GLOBAL(M_temp + right_gap_open_B,
                            Ix_scores[nB] + right_gap_extend_B,
                            Iy_scores[nB] + right_gap_open_B);
        Ix_scores[nB] = score;
        SELECT_SCORE_GLOBAL(M_scores[nB-1] + gap_open_A,
                            Iy_scores[nB-1] + gap_extend_A,
                            Ix_scores[nB-1] + gap_open_A);
        Iy_scores[nB] = score;
    }

    M_temp = M_scores[0];
    Ix_temp = Ix_scores[0];
    Iy_temp = Iy_scores[0];
    M_scores[0] = -DBL_MAX;
    Ix_scores[0] = left_gap_open_B + left_gap_extend_B * (i-1);
    Iy_scores[0] = -DBL_MAX;
    kA = CHARINDEX(sA[nA-1]);
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_SCORE_GLOBAL(M_temp,
                            Ix_temp,
                            Iy_temp);
        M_temp = M_scores[j];
        M_scores[j] = score + self->substitution_matrix[kA][kB];
        SELECT_SCORE_GLOBAL(M_temp + gap_open_B,
                            Ix_scores[j] + gap_extend_B,
                            Iy_scores[j] + gap_open_B);
        Ix_temp = Ix_scores[j];
        Ix_scores[j] = score;
        SELECT_SCORE_GLOBAL(M_scores[j-1] + right_gap_open_A,
                            Iy_scores[j-1] + right_gap_extend_A,
                            Ix_scores[j-1] + right_gap_open_A);
        Iy_temp = Iy_scores[j];
        Iy_scores[j] = score;
    }

    kB = CHARINDEX(sB[nB-1]);
    SELECT_SCORE_GLOBAL(M_temp,
                        Ix_temp,
                        Iy_temp);
    M_temp = M_scores[nB];
    M_scores[nB] = score + self->substitution_matrix[kA][kB];
    SELECT_SCORE_GLOBAL(M_temp + right_gap_open_B,
                        Ix_scores[nB] + right_gap_extend_B,
                        Iy_scores[nB] + right_gap_open_B);
    Ix_temp = Ix_scores[nB];
    Ix_scores[nB] = score;
    SELECT_SCORE_GLOBAL(M_scores[nB-1] + right_gap_open_A,
                        Ix_scores[nB-1] + right_gap_open_A,
                        Iy_scores[nB-1] + right_gap_extend_A);
    Iy_temp = Iy_scores[nB];
    Iy_scores[nB] = score;

    SELECT_SCORE_GLOBAL(M_scores[nB], Ix_scores[nB], Iy_scores[nB]);
    PyMem_Free(M_scores);
    PyMem_Free(Ix_scores);
    PyMem_Free(Iy_scores);
    return PyFloat_FromDouble(score);

exit:
    if (M_scores) PyMem_Free(M_scores);
    if (Ix_scores) PyMem_Free(Ix_scores);
    if (Iy_scores) PyMem_Free(Iy_scores);
    return PyErr_NoMemory();
}

static PyObject*
Aligner_gotoh_local_score(Aligner* self, const char* sA, Py_ssize_t nA,
                                         const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_open_A = self->target_open_gap_score;
    const double gap_open_B = self->query_open_gap_score;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    double* M_scores = NULL;
    double* Ix_scores = NULL;
    double* Iy_scores = NULL;
    double score;
    double temp;
    double M_temp;
    double Ix_temp;
    double Iy_temp;
    double maximum = 0.0;

    /* Gotoh algorithm with three states */
    M_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Iy_scores) goto exit;

    /* The top row of the score matrix is a special case,
     * as there are no previously aligned characters.
     */
    M_scores[0] = 0;
    Ix_scores[0] = -DBL_MAX;
    Iy_scores[0] = -DBL_MAX;
    for (j = 1; j <= nB; j++) {
        M_scores[j] = -DBL_MAX;
        Ix_scores[j] = -DBL_MAX;
        Iy_scores[j] = 0;
    }

    for (i = 1; i < nA; i++) {
        M_temp = M_scores[0];
        Ix_temp = Ix_scores[0];
        Iy_temp = Iy_scores[0];
        M_scores[0] = -DBL_MAX;
        Ix_scores[0] = 0;
        Iy_scores[0] = -DBL_MAX;
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp,
                                           Ix_temp,
                                           Iy_temp,
                                           self->substitution_matrix[kA][kB]);
            M_temp = M_scores[j];
            M_scores[j] = score;
            SELECT_SCORE_LOCAL3(M_temp + gap_open_B,
                                Ix_scores[j] + gap_extend_B,
                                Iy_scores[j] + gap_open_B);
            Ix_temp = Ix_scores[j];
            Ix_scores[j] = score;
            SELECT_SCORE_LOCAL3(M_scores[j-1] + gap_open_A,
                                Ix_scores[j-1] + gap_open_A,
                                Iy_scores[j-1] + gap_extend_A);
            Iy_temp = Iy_scores[j];
            Iy_scores[j] = score;
        }

        kB = CHARINDEX(sB[nB-1]);

        Ix_scores[nB] = 0;
        Iy_scores[nB] = 0;
        SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp,
                                       Ix_temp,
                                       Iy_temp,
                                       self->substitution_matrix[kA][kB]);
        M_temp = M_scores[nB];
        M_scores[nB] = score;
    }

    M_temp = M_scores[0];
    Ix_temp = Ix_scores[0];
    Iy_temp = Iy_scores[0];
    M_scores[0] = -DBL_MAX;
    Ix_scores[0] = 0;
    Iy_scores[0] = -DBL_MAX;
    kA = CHARINDEX(sA[nA-1]);
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp,
                                       Ix_temp,
                                       Iy_temp,
                                       self->substitution_matrix[kA][kB]);
        M_temp = M_scores[j];
        M_scores[j] = score;
        Ix_temp = Ix_scores[j];
        Iy_temp = Iy_scores[j];
        Ix_scores[j] = 0;
        Iy_scores[j] = 0;
    }

    kB = CHARINDEX(sB[nB-1]);
    SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp,
                                   Ix_temp,
                                   Iy_temp,
                                   self->substitution_matrix[kA][kB]);

    PyMem_Free(M_scores);
    PyMem_Free(Ix_scores);
    PyMem_Free(Iy_scores);
    return PyFloat_FromDouble(maximum);

exit:
    if (M_scores) PyMem_Free(M_scores);
    if (Ix_scores) PyMem_Free(Ix_scores);
    if (Iy_scores) PyMem_Free(Iy_scores);
    return PyErr_NoMemory();
}

static PyObject*
Aligner_gotoh_global_align(Aligner* self, const char* sA, Py_ssize_t nA,
                                          const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int kA;
    int kB;
    const double gap_open_A = self->target_open_gap_score;
    const double gap_open_B = self->query_open_gap_score;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double left_gap_open_A = self->target_left_open_gap_score;
    const double left_gap_open_B = self->query_left_open_gap_score;
    const double left_gap_extend_A = self->target_left_extend_gap_score;
    const double left_gap_extend_B = self->query_left_extend_gap_score;
    const double right_gap_open_A = self->target_right_open_gap_score;
    const double right_gap_open_B = self->query_right_open_gap_score;
    const double right_gap_extend_A = self->target_right_extend_gap_score;
    const double right_gap_extend_B = self->query_right_extend_gap_score;
    const double epsilon = self->epsilon;
    TraceGapsGotoh** gaps = NULL;
    Trace** M = NULL;
    double* M_scores = NULL;
    double* Ix_scores = NULL;
    double* Iy_scores = NULL;
    double score;
    int trace;
    double temp;
    double M_temp;
    double Ix_temp;
    double Iy_temp;
    PathGenerator* paths;

    /* Gotoh algorithm with three states */
    paths = PathGenerator_create_Gotoh(nA, nB, Global);
    if (!paths) return NULL;

    M_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Iy_scores) goto exit;
    M = paths->M;
    gaps = paths->gaps.gotoh;

    /* Gotoh algorithm with three states */
    M_scores[0] = 0;
    Ix_scores[0] = -DBL_MAX;
    Iy_scores[0] = -DBL_MAX;

    for (j = 1; j <= nB; j++) {
        M_scores[j] = -DBL_MAX;
        Ix_scores[j] = -DBL_MAX;
        Iy_scores[j] = left_gap_open_A + left_gap_extend_A * (j-1);
    }

    for (i = 1; i < nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        M_temp = M_scores[0];
        Ix_temp = Ix_scores[0];
        Iy_temp = Iy_scores[0];
        M_scores[0] = -DBL_MAX;
        Ix_scores[0] = left_gap_open_B + left_gap_extend_B * (i-1);
        Iy_scores[0] = -DBL_MAX;
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_TRACE_GOTOH_GLOBAL_ALIGN;
            M_temp = M_scores[j];
            M_scores[j] = score + self->substitution_matrix[kA][kB];
            SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix,
                                          M_temp + gap_open_B,
                                          Ix_scores[j] + gap_extend_B,
                                          Iy_scores[j] + gap_open_B);
            Ix_temp = Ix_scores[j];
            Ix_scores[j] = score;
            SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy,
                                          M_scores[j-1] + gap_open_A,
                                          Ix_scores[j-1] + gap_open_A,
                                          Iy_scores[j-1] + gap_extend_A);
            Iy_temp = Iy_scores[j];
            Iy_scores[j] = score;
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_TRACE_GOTOH_GLOBAL_ALIGN;
        M_temp = M_scores[nB];
        M_scores[nB] = score + self->substitution_matrix[kA][kB];
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix,
                                      M_temp + right_gap_open_B,
                                      Ix_scores[nB] + right_gap_extend_B,
                                      Iy_scores[nB] + right_gap_open_B);
        Ix_temp = Ix_scores[nB];
        Ix_scores[nB] = score;
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy,
                                      M_scores[nB-1] + gap_open_A,
                                      Ix_scores[nB-1] + gap_open_A,
                                      Iy_scores[nB-1] + gap_extend_A);
        Iy_temp = Iy_scores[nB];
        Iy_scores[nB] = score;
    }
    kA = CHARINDEX(sA[nA-1]);
    M_temp = M_scores[0];
    Ix_temp = Ix_scores[0];
    Iy_temp = Iy_scores[0];
    M_scores[0] = -DBL_MAX;
    Ix_scores[0] = left_gap_open_B + left_gap_extend_B * (nA-1);
    Iy_scores[0] = -DBL_MAX;
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_TRACE_GOTOH_GLOBAL_ALIGN;
        M_temp = M_scores[j];
        M_scores[j] = score + self->substitution_matrix[kA][kB];
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix,
                                      M_scores[j] + gap_open_B,
                                      Ix_scores[j] + gap_extend_B,
                                      Iy_scores[j] + gap_open_B);
        Ix_temp = Ix_scores[j];
        Ix_scores[j] = score;
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy,
                                      M_scores[j-1] + right_gap_open_A,
                                      Ix_scores[j-1] + right_gap_open_A,
                                      Iy_scores[j-1] + right_gap_extend_A);
        Iy_temp = Iy_scores[j];
        Iy_scores[j] = score;
    }
    kB = CHARINDEX(sB[nB-1]);
    SELECT_TRACE_GOTOH_GLOBAL_ALIGN;
    M_temp = M_scores[j];
    M_scores[j] = score + self->substitution_matrix[kA][kB];
    SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix,
                                  M_temp + right_gap_open_B,
                                  Ix_scores[j] + right_gap_extend_B,
                                  Iy_scores[j] + right_gap_open_B);
    Ix_scores[nB] = score;
    SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy,
                                  M_scores[j-1] + right_gap_open_A,
                                  Ix_scores[j-1] + right_gap_open_A,
                                  Iy_scores[j-1] + right_gap_extend_A);
    Iy_scores[nB] = score;
    M[nA][nB].path = 0;

    /* traceback */
    SELECT_SCORE_GLOBAL(M_scores[nB], Ix_scores[nB], Iy_scores[nB]);
    if (M_scores[nB] < score - epsilon) M[nA][nB].trace = 0;
    if (Ix_scores[nB] < score - epsilon) gaps[nA][nB].Ix = 0;
    if (Iy_scores[nB] < score - epsilon) gaps[nA][nB].Iy = 0;
    return Py_BuildValue("fN", score, paths);
exit:
    Py_DECREF(paths);
    if (M_scores) PyMem_Free(M_scores);
    if (Ix_scores) PyMem_Free(Ix_scores);
    if (Iy_scores) PyMem_Free(Iy_scores);
    return PyErr_NoMemory();
}

static PyObject*
Aligner_gotoh_local_align(Aligner* self, const char* sA, Py_ssize_t nA,
                                         const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int im = nA;
    int jm = nB;
    int kA;
    int kB;
    const double gap_open_A = self->target_open_gap_score;
    const double gap_open_B = self->query_open_gap_score;
    const double gap_extend_A = self->target_extend_gap_score;
    const double gap_extend_B = self->query_extend_gap_score;
    const double epsilon = self->epsilon;
    Trace** M = NULL;
    TraceGapsGotoh** gaps = NULL;
    double* M_scores = NULL;
    double* Ix_scores = NULL;
    double* Iy_scores = NULL;
    double score;
    int trace;
    double temp;
    double M_temp;
    double Ix_temp;
    double Iy_temp;
    double maximum = 0.0;
    PathGenerator* paths;

    /* Gotoh algorithm with three states */
    paths = PathGenerator_create_Gotoh(nA, nB, Local);
    if (!paths) return NULL;
    M = paths->M;
    gaps = paths->gaps.gotoh;

    M_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nB+1)*sizeof(double));
    if (!Iy_scores) goto exit;
    M_scores[0] = 0;
    Ix_scores[0] = -DBL_MAX;
    Iy_scores[0] = -DBL_MAX;

    for (j = 1; j <= nB; j++) {
        M_scores[j] = 0;
        Ix_scores[j] = -DBL_MAX;
        Iy_scores[j] = -DBL_MAX;
    }
    for (i = 1; i < nA; i++) {
        M_temp = M_scores[0];
        Ix_temp = Ix_scores[0];
        Iy_temp = Iy_scores[0];
        M_scores[0] = 0;
        Ix_scores[0] = -DBL_MAX;
        Iy_scores[0] = -DBL_MAX;
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j < nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_TRACE_GOTOH_LOCAL_ALIGN
            M_temp = M_scores[j];
            M_scores[j] = score;
            SELECT_TRACE_GOTOH_LOCAL_GAP(Ix,
                                     M_temp + gap_open_B,
                                     Ix_scores[j] + gap_extend_B,
                                     Iy_scores[j] + gap_open_B);
            Ix_temp = Ix_scores[j];
            Ix_scores[j] = score;
            SELECT_TRACE_GOTOH_LOCAL_GAP(Iy,
                                     M_scores[j-1] + gap_open_A,
                                     Ix_scores[j-1] + gap_open_A,
                                     Iy_scores[j-1] + gap_extend_A);
            Iy_temp = Iy_scores[j];
            Iy_scores[j] = score;
        }
        kB = CHARINDEX(sB[nB-1]);
        SELECT_TRACE_GOTOH_LOCAL_ALIGN
        M_temp = M_scores[j];
        M_scores[j] = score;
        Ix_temp = Ix_scores[nB];
        Ix_scores[nB] = 0;
        gaps[i][nB].Ix = 0;
        Iy_temp = Iy_scores[nB];
        Iy_scores[nB] = 0;
        gaps[i][nB].Iy = 0;
    }
    M_temp = M_scores[0];
    M_scores[0] = 0;
    M[nA][0].trace = 0;
    Ix_temp = Ix_scores[0];
    Ix_scores[0] = -DBL_MAX;
    gaps[nA][0].Ix = 0;
    gaps[nA][0].Iy = 0;
    Iy_temp = Iy_scores[0];
    Iy_scores[0] = -DBL_MAX;
    kA = CHARINDEX(sA[nA-1]);
    for (j = 1; j < nB; j++) {
        kB = CHARINDEX(sB[j-1]);
        SELECT_TRACE_GOTOH_LOCAL_ALIGN
        M_temp = M_scores[j];
        M_scores[j] = score;
        Ix_temp = Ix_scores[j];
        Ix_scores[j] = 0;
        gaps[nA][j].Ix = 0;
        Iy_temp = Iy_scores[j];
        Iy_scores[j] = 0;
        gaps[nA][j].Iy = 0;
    }
    kB = CHARINDEX(sB[nB-1]);
    SELECT_TRACE_GOTOH_LOCAL_ALIGN
    gaps[nA][nB].Ix = 0;
    gaps[nA][nB].Iy = 0;

    PyMem_Free(M_scores);
    PyMem_Free(Ix_scores);
    PyMem_Free(Iy_scores);

    /* traceback */
    if (maximum == 0) M[0][0].path = DONE;
    return Py_BuildValue("fN", maximum, paths);
exit:
    Py_DECREF(paths);
    if (M_scores) PyMem_Free(M_scores);
    if (Ix_scores) PyMem_Free(Ix_scores);
    if (Iy_scores) PyMem_Free(Iy_scores);
    return PyErr_NoMemory();
}

static int
_call_query_gap_function(Aligner* aligner, int i, int j, double* score)
{
    double value;
    PyObject* result;
    PyObject* function = aligner->query_gap_function;
    if (!function)
        value = aligner->query_open_gap_score
              + (j-1) * aligner->query_extend_gap_score;
    else {
        result = PyObject_CallFunction(function, "ii", i, j);
        if (result == NULL) return 0;
        value = PyFloat_AsDouble(result);
        Py_DECREF(result);
        if (value == -1.0 && PyErr_Occurred()) return 0;
    }
    *score = value;
    return 1;
}

static int
_call_target_gap_function(Aligner* aligner, int i, int j, double* score)
{
    double value;
    PyObject* result;
    PyObject* function = aligner->target_gap_function;
    if (!function)
        value = aligner->target_open_gap_score
              + (j-1) * aligner->target_extend_gap_score;
    else {
        result = PyObject_CallFunction(function, "ii", i, j);
        if (result == NULL) return 0;
        value = PyFloat_AsDouble(result);
        Py_DECREF(result);
        if (value == -1.0 && PyErr_Occurred()) return 0;
    }
    *score = value;
    return 1;
}

static PyObject*
Aligner_waterman_smith_beyer_global_score(Aligner* self,
                                          const char* sA, Py_ssize_t nA,
                                          const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int k;
    int kA;
    int kB;
    double** M = NULL;
    double** Ix = NULL;
    double** Iy = NULL;
    double score = 0.0;
    double gapscore;
    double temp;
    int ok = 1;

    /* Waterman-Smith-Beyer algorithm */
    M = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!M) goto exit;
    Ix = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Ix) goto exit;
    Iy = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Iy) goto exit;
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!M[i]) goto exit;
        Ix[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Ix[i]) goto exit;
        Iy[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Iy[i]) goto exit;
    }

    /* The top row of the score matrix is a special case,
     *  as there are no previously aligned characters.
     */
    M[0][0] = 0;
    Ix[0][0] = -DBL_MAX;
    Iy[0][0] = -DBL_MAX;
    for (i = 1; i <= nA; i++) {
        ok = _call_query_gap_function(self, 0, i, &score);
        if (!ok) goto exit;
        M[i][0] = -DBL_MAX;
        Ix[i][0] = score;
        Iy[i][0] = -DBL_MAX;
    }
    for (j = 1; j <= nB; j++) {
        ok = _call_target_gap_function(self, 0, j, &score);
        if (!ok) goto exit;
        M[0][j] = -DBL_MAX;
        Ix[0][j] = -DBL_MAX;
        Iy[0][j] = score;
    }
    for (i = 1; i <= nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j <= nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_GLOBAL(M[i-1][j-1], Ix[i-1][j-1], Iy[i-1][j-1]);
            M[i][j] = score + self->substitution_matrix[kA][kB];
            score = -DBL_MAX;
            for (k = 1; k <= i; k++) {
                ok = _call_query_gap_function(self, j, k, &gapscore);
                if (!ok) goto exit;
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i-k][j], Iy[i-k][j]);
            }
            Ix[i][j] = score;
            score = -DBL_MAX;
            for (k = 1; k <= j; k++) {
                ok = _call_target_gap_function(self, i, k, &gapscore);
                if (!ok) goto exit;
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i][j-k], Ix[i][j-k]);
            }
            Iy[i][j] = score;
        }
    }
    SELECT_SCORE_GLOBAL(M[nA][nB], Ix[nA][nB], Iy[nA][nB]);

exit:
    if (M) {
        /* If M is NULL, then Ix is also NULL. */
        if (Ix) {
            /* If Ix is NULL, then Iy is also NULL. */
            if (Iy) {
                /* If Iy is NULL, then M[i], Ix[i], and Iy[i] are also NULL. */ 
                for (i = 0; i <= nA; i++) {
                    if (!M[i]) break;
                    PyMem_Free(M[i]); 
                    if (!Ix[i]) break;
                    PyMem_Free(Ix[i]);
                    if (!Iy[i]) break;
                    PyMem_Free(Iy[i]);
                }
                PyMem_Free(Iy);
            }
            PyMem_Free(Ix);
        }
        PyMem_Free(M);
    }
    if (!ok) return NULL;
    return PyFloat_FromDouble(score);
}

static PyObject*
Aligner_waterman_smith_beyer_global_align(Aligner* self,
                                          const char* sA, Py_ssize_t nA,
                                          const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int gap;
    int kA;
    int kB;
    const double epsilon = self->epsilon;
    Trace** M;
    TraceGapsWatermanSmithBeyer** gaps;
    double** M_scores = NULL;
    double** Ix_scores = NULL;
    double** Iy_scores = NULL;
    int ng;
    int nm;
    double score;
    double gapscore;
    double temp;
    int trace;
    int* gapM;
    int* gapXY;
    int ok = 1;
    PathGenerator* paths = NULL;

    /* Waterman-Smith-Beyer algorithm */
    paths = PathGenerator_create_WSB(nA, nB, Global);
    if (!paths) return NULL;
    M = paths->M;
    gaps = paths->gaps.waterman_smith_beyer;

    M_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Iy_scores) goto exit;
    for (i = 0; i <= nA; i++) {
        M_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!M_scores[i]) goto exit;
        M_scores[i][0] = -DBL_MAX;
        Ix_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Ix_scores[i]) goto exit;
        Ix_scores[i][0] = 0;
        Iy_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Iy_scores[i]) goto exit;
        Iy_scores[i][0] = -DBL_MAX;
    }
    M_scores[0][0] = 0;
    Ix_scores[0][0] = -DBL_MAX;
    for (i = 1; i <= nB; i++) {
        M_scores[0][i] = -DBL_MAX;
        Ix_scores[0][i] = -DBL_MAX;
        Iy_scores[0][i] = 0;
    }
    for (i = 1; i <= nA; i++) {
        ok = _call_query_gap_function(self, 0, i, &score);
        if (!ok) goto exit;
        Ix_scores[i][0] = score;
    }
    for (j = 1; j <= nB; j++) {
        ok = _call_target_gap_function(self, 0, j, &score);
        if (!ok) goto exit;
        Iy_scores[0][j] = score;
    }
    for (i = 1; i <= nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j <= nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_TRACE_WATERMAN_SMITH_BEYER_GLOBAL_ALIGN(self->substitution_matrix[kA][kB]);
            gapM = PyMem_Malloc((i+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIx = gapM;
            gapXY = PyMem_Malloc((i+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IyIx = gapXY;
            nm = 0;
            ng = 0;
            score = -DBL_MAX;
            for (gap = 1; gap <= i; gap++) {
                ok = _call_query_gap_function(self, j, gap, &gapscore);
                if (!ok) goto exit;
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_scores[i-gap][j],
                                                      Iy_scores[i-gap][j]);
            }
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIx = gapM;
            gapM[nm] = 0;
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int));
            if (!gapXY) goto exit;
            gapXY[ng] = 0;
            gaps[i][j].IyIx = gapXY;
            Ix_scores[i][j] = score;
            gapM = PyMem_Malloc((j+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIy = gapM;
            gapXY = PyMem_Malloc((j+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IxIy = gapXY;
            nm = 0;
            ng = 0;
            score = -DBL_MAX;
            for (gap = 1; gap <= j; gap++) {
                ok = _call_target_gap_function(self, i, gap, &gapscore);
                if (!ok) goto exit;
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_scores[i][j-gap],
                                                      Ix_scores[i][j-gap]);
            }
            Iy_scores[i][j] = score;
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIy = gapM;
            gapM[nm] = 0;
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IxIy = gapXY;
            gapXY[ng] = 0;
        }
    }

    /* traceback */
    SELECT_SCORE_GLOBAL(M_scores[nA][nB], Ix_scores[nA][nB], Iy_scores[nA][nB]);
    M[nA][nB].path = 0;

    if (M_scores[nA][nB] < score - epsilon) M[nA][nB].trace = 0;
    if (Ix_scores[nA][nB] < score - epsilon) {
        gapM = PyMem_Realloc(gaps[nA][nB].MIx, sizeof(int));
        if (!gapM) goto exit;
        gapM[0] = 0;
        gaps[nA][nB].MIx = gapM;
        gapXY = PyMem_Realloc(gaps[nA][nB].IyIx, sizeof(int));
        if (!gapXY) goto exit;
        gapXY[0] = 0;
        gaps[nA][nB].IyIx = gapXY;
    }
    if (Iy_scores[nA][nB] < score - epsilon) {
        gapM = PyMem_Realloc(gaps[nA][nB].MIy, sizeof(int));
        if (!gapM) goto exit;
        gapM[0] = 0;
        gaps[nA][nB].MIy = gapM;
        gapXY = PyMem_Realloc(gaps[nA][nB].IxIy, sizeof(int));
        if (!gapXY) goto exit;
        gapXY[0] = 0;
        gaps[nA][nB].IxIy = gapXY;
    }
    for (i = 0; i <= nA; i++) {
        PyMem_Free(M_scores[i]);
        PyMem_Free(Ix_scores[i]);
        PyMem_Free(Iy_scores[i]);
    }
    PyMem_Free(M_scores);
    PyMem_Free(Ix_scores);
    PyMem_Free(Iy_scores);
    return Py_BuildValue("fN", score, paths);

exit:
    if (ok) /* otherwise, an exception was already set */
        PyErr_SetNone(PyExc_MemoryError);
    Py_DECREF(paths);
    if (M_scores) {
        /* If M is NULL, then Ix is also NULL. */
        if (Ix_scores) {
            /* If Ix is NULL, then Iy is also NULL. */
            if (Iy_scores) {
                /* If Iy is NULL, then M[i], Ix[i], and Iy[i] are also NULL. */
                for (i = 0; i <= nA; i++) {
                    if (!M_scores[i]) break;
                    PyMem_Free(M_scores[i]);
                    if (!Ix_scores[i]) break;
                    PyMem_Free(Ix_scores[i]);
                    if (!Iy_scores[i]) break;
                    PyMem_Free(Iy_scores[i]);
                }
                PyMem_Free(Iy_scores);
            }
            PyMem_Free(Ix_scores);
        }
        PyMem_Free(M_scores);
    }
    return NULL;
}

static PyObject*
Aligner_waterman_smith_beyer_local_score(Aligner* self,
                                         const char* sA, Py_ssize_t nA,
                                         const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int gap;
    int kA;
    int kB;
    double** M = NULL;
    double** Ix = NULL;
    double** Iy = NULL;
    double score = 0.0;
    double gapscore = 0.0;
    double temp;
    int ok = 1;

    double maximum = 0.0;
    PyObject* result = NULL;

    /* Waterman-Smith-Beyer algorithm */
    M = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!M) goto exit;
    Ix = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Ix) goto exit;
    Iy = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Iy) goto exit;
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!M[i]) goto exit;
        Ix[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Ix[i]) goto exit;
        Iy[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Iy[i]) goto exit;
    }

    /* The top row of the score matrix is a special case,
     *  as there are no previously aligned characters.
     */
    M[0][0] = 0;
    Ix[0][0] = -DBL_MAX;
    Iy[0][0] = -DBL_MAX;
    for (i = 1; i <= nA; i++) {
        M[i][0] = -DBL_MAX;
        Ix[i][0] = 0;
        Iy[i][0] = -DBL_MAX;
    }
    for (j = 1; j <= nB; j++) {
        M[0][j] = -DBL_MAX;
        Ix[0][j] = -DBL_MAX;
        Iy[0][j] = 0;
    }
    for (i = 1; i <= nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j <= nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            SELECT_SCORE_GOTOH_LOCAL_ALIGN(M[i-1][j-1],
                                           Ix[i-1][j-1],
                                           Iy[i-1][j-1],
                                           self->substitution_matrix[kA][kB]);
            M[i][j] = score;
            if (i == nA || j == nB) {
                Ix[i][j] = 0;
                Iy[i][j] = 0;
                continue;
            }
            score = 0.0;
            for (gap = 1; gap <= i; gap++) {
                ok = _call_query_gap_function(self, j, gap, &gapscore);
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i-gap][j], Iy[i-gap][j]);
                if (!ok) goto exit;
            }
            if (score > maximum) maximum = score;
            Ix[i][j] = score;
            score = 0.0;
            for (gap = 1; gap <= j; gap++) {
                ok = _call_target_gap_function(self, i, gap, &gapscore);
                if (!ok) goto exit;
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i][j-gap], Ix[i][j-gap]);
            }
            if (score > maximum) maximum = score;
            Iy[i][j] = score;
        }
    }
    SELECT_SCORE_GLOBAL(M[nA][nB], Ix[nA][nB], Iy[nA][nB]);
    if (score > maximum) maximum = score;

    result = PyFloat_FromDouble(maximum);
exit:
    if (M) {
        /* If M is NULL, then Ix is also NULL. */
        if (Ix) {
            /* If Ix is NULL, then Iy is also NULL. */
            if (Iy) {
                /* If Iy is NULL, then M[i], Ix[i], and Iy[i] are
                 * also NULL. */
                for (i = 0; i <= nA; i++) {
                    if (!M[i]) break;
                    PyMem_Free(M[i]);
                    if (!Ix[i]) break;
                    PyMem_Free(Ix[i]);
                    if (!Iy[i]) break;
                    PyMem_Free(Iy[i]);
                }
                PyMem_Free(Iy);
            }
            PyMem_Free(Ix);
        }
        PyMem_Free(M);
    }
    if (!ok) return NULL;
    if (!result) return PyErr_NoMemory();
    return result;
}

static PyObject*
Aligner_waterman_smith_beyer_local_align(Aligner* self,
                                         const char* sA, Py_ssize_t nA,
                                         const char* sB, Py_ssize_t nB)
{
    char c;
    int i;
    int j;
    int im = nA;
    int jm = nB;
    int gap;
    int kA;
    int kB;
    const double epsilon = self->epsilon;
    Trace** M = NULL;
    TraceGapsWatermanSmithBeyer** gaps;
    double** M_scores;
    double** Ix_scores;
    double** Iy_scores;
    double score;
    double gapscore;
    double temp;
    int trace;
    int* gapM;
    int* gapXY;
    int nm;
    int ng;
    int ok = 1;
    double maximum = 0;

    PathGenerator* paths = NULL;

    /* Waterman-Smith-Beyer algorithm */
    paths = PathGenerator_create_WSB(nA, nB, Local);
    if (!paths) return NULL;
    M = paths->M;
    gaps = paths->gaps.waterman_smith_beyer;

    M_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!M_scores) goto exit;
    Ix_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Ix_scores) goto exit;
    Iy_scores = PyMem_Malloc((nA+1)*sizeof(double*));
    if (!Iy_scores) goto exit;
    for (i = 0; i <= nA; i++) {
        M_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!M_scores[i]) goto exit;
        M_scores[i][0] = 0;
        Ix_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Ix_scores[i]) goto exit;
        Ix_scores[i][0] = -DBL_MAX;
        Iy_scores[i] = PyMem_Malloc((nB+1)*sizeof(double));
        if (!Iy_scores[i]) goto exit;
        Iy_scores[i][0] = -DBL_MAX;
    }
    for (i = 1; i <= nB; i++) {
        M_scores[0][i] = 0;
        Ix_scores[0][i] = -DBL_MAX;
        Iy_scores[0][i] = -DBL_MAX;
    }
    for (i = 1; i <= nA; i++) {
        kA = CHARINDEX(sA[i-1]);
        for (j = 1; j <= nB; j++) {
            kB = CHARINDEX(sB[j-1]);
            nm = 0;
            ng = 0;
            SELECT_TRACE_WATERMAN_SMITH_BEYER_ALIGN(
                                           M_scores[i-1][j-1],
                                           Ix_scores[i-1][j-1],
                                           Iy_scores[i-1][j-1],
                                           self->substitution_matrix[kA][kB]);
            M[i][j].path = 0;
            if (i == nA || j == nB) {
                Ix_scores[i][j] = score;
                gaps[i][j].MIx = NULL;
                gaps[i][j].IyIx = NULL;
                gaps[i][j].MIy = NULL;
                gaps[i][j].IxIy = NULL;
                Iy_scores[i][j] = score;
                continue;
            }
            gapM = PyMem_Malloc((i+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIx = gapM;
            gapXY = PyMem_Malloc((i+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IyIx = gapXY;
            score = -DBL_MAX;
            for (gap = 1; gap <= i; gap++) {
                ok = _call_query_gap_function(self, j, gap, &gapscore);
                if (!ok) goto exit;
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_scores[i-gap][j],
                                                      Iy_scores[i-gap][j]);
            }
            if (score < epsilon) {
                score = -DBL_MAX;
                nm = 0;
                ng = 0;
            }
            else if (score > maximum) maximum = score;
            gapM[nm] = 0;
            gapXY[ng] = 0;
            Ix_scores[i][j] = score;
            M[i][j].path = 0;
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIx = gapM;
            gapM[nm] = 0;
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IyIx = gapXY;
            gapXY[ng] = 0;
            gapM = PyMem_Malloc((j+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIy = gapM;
            gapXY = PyMem_Malloc((j+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IxIy = gapXY;
            nm = 0;
            ng = 0;
            score = -DBL_MAX;
            gapM[0] = 0;
            for (gap = 1; gap <= j; gap++) {
                ok = _call_target_gap_function(self, i, gap, &gapscore);
                if (!ok) goto exit;
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_scores[i][j-gap],
                                                      Ix_scores[i][j-gap]);
            }
            if (score < epsilon) {
                score = -DBL_MAX;
                nm = 0;
                ng = 0;
            }
            else if (score > maximum) maximum = score;
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int));
            if (!gapM) goto exit;
            gaps[i][j].MIy = gapM;
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int));
            if (!gapXY) goto exit;
            gaps[i][j].IxIy = gapXY;
            gapM[nm] = 0;
            gapXY[ng] = 0;
            Iy_scores[i][j] = score;
            M[i][j].path = 0;
        }
    }
    for (i = 0; i <= nA; i++) PyMem_Free(M_scores[i]);
    PyMem_Free(M_scores);
    for (i = 0; i <= nA; i++) PyMem_Free(Ix_scores[i]);
    PyMem_Free(Ix_scores);
    for (i = 0; i <= nA; i++) PyMem_Free(Iy_scores[i]);
    PyMem_Free(Iy_scores);

    /* traceback */
    if (maximum == 0) M[0][0].path = DONE;
    return Py_BuildValue("fN", maximum, paths);

exit:
    if (ok) /* otherwise, an exception was already set */
        PyErr_SetNone(PyExc_MemoryError);
    Py_DECREF(paths);
    if (M_scores) {
        /* If M is NULL, then Ix is also NULL. */
        if (Ix_scores) {
            /* If Ix is NULL, then Iy is also NULL. */
            if (Iy_scores) {
                /* If Iy is NULL, then M[i], Ix[i], and Iy[i] are also NULL. */
                for (i = 0; i <= nA; i++) {
                    if (!M_scores[i]) break;
                    PyMem_Free(M_scores[i]);
                    if (!Ix_scores[i]) break;
                    PyMem_Free(Ix_scores[i]);
                    if (!Iy_scores[i]) break;
                    PyMem_Free(Iy_scores[i]);
                }
                PyMem_Free(Iy_scores);
            }
            PyMem_Free(Ix_scores);
        }
        PyMem_Free(M_scores);
    }
    return NULL;
}
 
static const char Aligner_score__doc__[] = "calculates the alignment score";

static PyObject*
Aligner_score(Aligner* self, PyObject* args, PyObject* keywords)
{
    const char* sA;
    const char* sB;
    Py_ssize_t nA;
    Py_ssize_t nB;
    const Mode mode = self->mode;
    const Algorithm algorithm = _get_algorithm(self);

    static char *kwlist[] = {"sequenceA", "sequenceB", NULL};
    if(!PyArg_ParseTupleAndKeywords(args, keywords, "s#s#", kwlist,
                                    &sA, &nA, &sB, &nB))
        return NULL;

    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    return Aligner_needlemanwunsch_score(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_smithwaterman_score(self, sA, nA, sB, nB);
            }
        case Gotoh:
            switch (mode) {
                case Global:
                    return Aligner_gotoh_global_score(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_gotoh_local_score(self, sA, nA, sB, nB);
            }
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    return Aligner_waterman_smith_beyer_global_score(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_waterman_smith_beyer_local_score(self, sA, nA, sB, nB);
            }
        case Unknown:
        default:
            PyErr_SetString(PyExc_RuntimeError, "unknown algorithm");
            return NULL;
    }
}

static const char Aligner_align__doc__[] = "align two sequences";

static PyObject*
Aligner_align(Aligner* self, PyObject* args, PyObject* keywords)
{
    const char* sA;
    const char* sB;
    Py_ssize_t nA;
    Py_ssize_t nB;
    const Mode mode = self->mode;
    const Algorithm algorithm = _get_algorithm(self);

    static char *kwlist[] = {"sequenceA", "sequenceB", NULL};
    if(!PyArg_ParseTupleAndKeywords(args, keywords, "s#s#", kwlist,
                                    &sA, &nA, &sB, &nB))
        return NULL;

    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    return Aligner_needlemanwunsch_align(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_smithwaterman_align(self, sA, nA, sB, nB);
            }
        case Gotoh:
            switch (mode) {
                case Global:
                    return Aligner_gotoh_global_align(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_gotoh_local_align(self, sA, nA, sB, nB);
            }
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    return Aligner_waterman_smith_beyer_global_align(self, sA, nA, sB, nB);
                case Local:
                    return Aligner_waterman_smith_beyer_local_align(self, sA, nA, sB, nB);
            }
        case Unknown:
        default:
            PyErr_SetString(PyExc_RuntimeError, "unknown algorithm");
            return NULL;
    }
}

static char Aligner_doc[] =
"Aligner.\n";

static PyMethodDef Aligner_methods[] = {
    {"score",
     (PyCFunction)Aligner_score,
     METH_VARARGS | METH_KEYWORDS,
     Aligner_score__doc__
    },
    {"align",
     (PyCFunction)Aligner_align,
     METH_VARARGS | METH_KEYWORDS,
     Aligner_align__doc__
    },
    {NULL}  /* Sentinel */
};

static PyTypeObject AlignerType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "_algorithms.PairwiseAligner", /* tp_name */
    sizeof(Aligner),               /* tp_basicsize */
    0,                             /* tp_itemsize */
    (destructor)Aligner_dealloc,   /* tp_dealloc */
    0,                             /* tp_print */
    0,                             /* tp_getattr */
    0,                             /* tp_setattr */
    0,                             /* tp_compare */
    (reprfunc)Aligner_repr,        /* tp_repr */
    0,                             /* tp_as_number */
    0,                             /* tp_as_sequence */
    0,                             /* tp_as_mapping */
    0,                             /* tp_hash */
    0,                             /* tp_call */
    (reprfunc)Aligner_str,         /* tp_str */
    0,                             /* tp_getattro */
    0,                             /* tp_setattro */
    0,                             /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,        /*tp_flags*/
    Aligner_doc,                   /* tp_doc */
    0,                             /* tp_traverse */
    0,                             /* tp_clear */
    0,                             /* tp_richcompare */
    0,                             /* tp_weaklistoffset */
    0,                             /* tp_iter */
    0,                             /* tp_iternext */
    Aligner_methods,               /* tp_methods */
    0,                             /* tp_members */
    Aligner_getset,                /* tp_getset */
    0,                             /* tp_base */
    0,                             /* tp_dict */
    0,                             /* tp_descr_get */
    0,                             /* tp_descr_set */
    0,                             /* tp_dictoffset */
    (initproc)Aligner_init,        /* tp_init */
};

/* Module definition */

static PyMethodDef _aligners_methods[] = {
    {NULL, NULL, 0, NULL}
};

static char _aligners__doc__[] =
"C extension module implementing pairwise alignment algorithms";

#if PY_MAJOR_VERSION >= 3

static struct PyModuleDef moduledef = {
        PyModuleDef_HEAD_INIT,
        "_aligners",
        _aligners__doc__,
        -1,
        _aligners_methods,
        NULL,
        NULL,
        NULL,
        NULL
};

PyObject *
PyInit__aligners(void)

#else

void
init_aligners(void)
#endif

{
  PyObject* module;

  AlignerType.tp_new = PyType_GenericNew;

  if (PyType_Ready(&AlignerType) < 0
   || PyType_Ready(&PathGenerator_Type) < 0)
#if PY_MAJOR_VERSION >= 3
      return NULL;
#else
      return;
#endif

#if PY_MAJOR_VERSION >= 3
    module = PyModule_Create(&moduledef);
#else
    module = Py_InitModule3("_aligners", _aligners_methods, _aligners__doc__);
#endif

  Py_INCREF(&AlignerType);
  PyModule_AddObject(module, "PairwiseAligner", (PyObject*) &AlignerType);

#if PY_MAJOR_VERSION >= 3
  return module;
#endif
}
