/* Copyright 2018-2025 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>
#include <stdbool.h>
#include "_pairwisealigner.h"
#include "substitution_matrices/_arraycore.h"


static bool warned = false;  // FIXME remove once Biopython 1.87 is out.


#define STARTPOINT 0x8
#define ENDPOINT 0x10
#define M_MATRIX 0x1
#define Ix_MATRIX 0x2
#define Iy_MATRIX 0x4
#define DONE 0x3
#define NONE 0x7

#define OVERFLOW_ERROR -1
#define MEMORY_ERROR -2
#define OTHER_ERROR -3

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

static PyTypeObject *Array_Type = NULL;
/* this will be set when initializing the module */


#define ERR_UNEXPECTED_MODE \
    PyErr_Format(PyExc_RuntimeError, "mode has unexpected value (in "__FILE__" on line %d)", __LINE__);

#define ERR_UNEXPECTED_ALGORITHM \
    PyErr_Format(PyExc_RuntimeError, "algorithm has unexpected value (in "__FILE__" on line %d)", __LINE__);

typedef struct {
    unsigned char trace : 5;
    unsigned char path : 3;
} Trace;

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

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

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;
    unsigned char strand;
} PathGenerator;

static PyObject*
PathGenerator_create_path(PathGenerator* self, int i, int j) {
    PyObject* tuple;
    PyObject* target_row;
    PyObject* query_row;
    PyObject* value;
    int path;
    int k, l;
    int n = 1;
    int direction = 0;
    Trace** M = self->M;
    const unsigned char strand = self->strand;

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

    direction = 0;
    tuple = PyTuple_New(2);
    if (!tuple) return NULL;
    target_row = PyTuple_New(n);
    query_row = PyTuple_New(n);
    PyTuple_SET_ITEM(tuple, 0, target_row);
    PyTuple_SET_ITEM(tuple, 1, query_row);

    if (target_row && query_row) {
        k = 0;
        switch (strand) {
            case '+':
                while (1) {
                    path = M[i][j].path;
                    if (path != direction) {
                        value = PyLong_FromLong(i);
                        if (!value) break;
                        PyTuple_SET_ITEM(target_row, k, value);
                        value = PyLong_FromLong(j);
                        if (!value) break;
                        PyTuple_SET_ITEM(query_row, k, value);
                        k++;
                        direction = path;
                    }
                    switch (path) {
                        case HORIZONTAL: j++; break;
                        case VERTICAL: i++; break;
                        case DIAGONAL: i++; j++; break;
                        default: return tuple;
                    }
                }
                break;
            case '-': {
                const int nB = self->nB;
                while (1) {
                    path = M[i][j].path;
                    if (path != direction) {
                        value = PyLong_FromLong(i);
                        if (!value) break;
                        PyTuple_SET_ITEM(target_row, k, value);
                        value = PyLong_FromLong(nB-j);
                        if (!value) break;
                        PyTuple_SET_ITEM(query_row, k, value);
                        k++;
                        direction = path;
                    }
                    switch (path) {
                        case HORIZONTAL: j++; break;
                        case VERTICAL: i++; break;
                        case DIAGONAL: i++; j++; break;
                        default: return tuple;
                    }
                }
                break;
            }
        }
    }
    Py_DECREF(tuple); /* all references were stolen */
    return PyErr_NoMemory();
}

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 = MEMORY_ERROR;
    Py_ssize_t temp;
    Py_ssize_t* counts;
    counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!counts) goto exit;
    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;
        }
    }
    PyMem_Free(counts);
exit:
    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 = MEMORY_ERROR;
    Py_ssize_t total = 0;
    Py_ssize_t temp;
    Py_ssize_t* counts;
    counts = PyMem_Malloc((nB+1)*sizeof(Py_ssize_t));
    if (!counts) goto exit;
    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 && (trace & STARTPOINT)) count = 1;
            counts[j] = count;
        }
    }
    count = total;
    PyMem_Free(counts);
exit:
    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 && (trace & STARTPOINT)) 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 && (trace & STARTPOINT)) 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++;
                }
            }
            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 = 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_fogsaa_length(PathGenerator* self)
{
    return 1;
}

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.
                         */
                        ERR_UNEXPECTED_MODE
                        return OTHER_ERROR;
                }
                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.
                         */
                        ERR_UNEXPECTED_MODE
                        return OTHER_ERROR;
                }
                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.
                         */
                        ERR_UNEXPECTED_MODE
                        return OTHER_ERROR;
                }
                break;
            case FOGSAA:
                if (self->mode != FOGSAA_Mode) {
                    ERR_UNEXPECTED_MODE
                    return OTHER_ERROR;
                }
                length = PathGenerator_fogsaa_length(self);
                break;
            case Unknown:
            default:
                ERR_UNEXPECTED_ALGORITHM
                return OTHER_ERROR;
        }
        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;
        case OTHER_ERROR:
        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:
        case FOGSAA:
            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 PathGenerator_create_path(self, 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 end 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 if (trace & STARTPOINT) {
            self->iA = i;
            self->iB = j;
            return PathGenerator_create_path(self, i, j);
        }
        else {
            PyErr_SetString(PyExc_RuntimeError,
                "Unexpected trace in PathGenerator_next_smithwaterman");
            return NULL;
        }
        trace = M[i][j].trace;
    }
}

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 PathGenerator_create_path(self, 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 == STARTPOINT) {
            self->iA = i;
            self->iB = j;
            return PathGenerator_create_path(self, 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;
        else {
            PyErr_SetString(PyExc_RuntimeError,
                "Unexpected trace in PathGenerator_next_gotoh_local");
            return NULL;
        }
        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 PathGenerator_create_path(self, 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 if (trace == STARTPOINT) {
                    self->iA = i;
                    self->iB = j;
                    return PathGenerator_create_path(self, i, j);
                }
                else {
                    PyErr_SetString(PyExc_RuntimeError,
                        "Unexpected trace in PathGenerator_next_waterman_smith_beyer_local");
                    return NULL;
                }
                M[iA][iB].path = DIAGONAL;
                break;
        }
        i = iA;
        j = iB;
    }
}

static PyObject*
PathGenerator_next_FOGSAA(PathGenerator* self)
{
    /* No need to create path because FOGSAA only finds one optimal alignment
     * the .path fields should be populated by FOGSAA_EXIT_ALIGN. To indicate
     * we've exhausted the iterator, just set self->M[0][0].path to DONE */
    Trace *last = &self->M[self->nA][self->nB];
    PyObject *path;

    if (last->path == DONE) {
        return NULL;
    }

    path = PathGenerator_create_path(self, 0, 0);
    last->path = DONE;
    return path;
}

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);
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
        case Gotoh:
            switch (mode) {
                case Global:
                    return PathGenerator_next_gotoh_global(self);
                case Local:
                    return PathGenerator_next_gotoh_local(self);
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    return PathGenerator_next_waterman_smith_beyer_global(self);
                case Local:
                    return PathGenerator_next_waterman_smith_beyer_local(self);
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
        case FOGSAA:
            return PathGenerator_next_FOGSAA(self);
            break;
        case Unknown:
        default:
            ERR_UNEXPECTED_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;
            }
            break;
        }
        case FOGSAA_Mode:
            self->M[self->nA][self->nB].path = 0;
            break;
    }
    Py_INCREF(Py_None);
    return Py_None;
}

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

static PySequenceMethods PathGenerator_as_sequence = {
    .sq_length = (lenfunc)PathGenerator_length,
};

static PyTypeObject PathGenerator_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "Path generator",
    .tp_basicsize = sizeof(PathGenerator),
    .tp_dealloc = (destructor)PathGenerator_dealloc,
    .tp_as_sequence = &PathGenerator_as_sequence,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_iter = PyObject_SelfIter,
    .tp_iternext = (iternextfunc)PathGenerator_next,
    .tp_methods = PathGenerator_methods,
};

static Algorithm _get_algorithm(Aligner* self)
{
    Algorithm algorithm = self->algorithm;
    if (algorithm == Unknown) {
        const double open_internal_insertion_score = self->open_internal_insertion_score;
        const double open_internal_deletion_score = self->open_internal_deletion_score;
        const double extend_internal_insertion_score = self->extend_internal_insertion_score;
        const double extend_internal_deletion_score = self->extend_internal_deletion_score;
        const double open_left_insertion_score = self->open_left_insertion_score;
        const double extend_left_insertion_score = self->extend_left_insertion_score;
        const double open_left_deletion_score = self->open_left_deletion_score;
        const double open_right_insertion_score = self->open_right_insertion_score;
        const double open_right_deletion_score = self->open_right_deletion_score;
        const double extend_right_insertion_score = self->extend_right_insertion_score;
        const double extend_left_deletion_score = self->extend_left_deletion_score;
        const double extend_right_deletion_score = self->extend_right_deletion_score;
        if (self->mode == FOGSAA_Mode)
            algorithm = FOGSAA;
        else if (self->insertion_score_function || self->deletion_score_function)
            algorithm = WatermanSmithBeyer;
        else if (open_internal_insertion_score == extend_internal_insertion_score
              && open_internal_deletion_score == extend_internal_deletion_score
              && open_left_insertion_score == extend_left_insertion_score
              && open_right_insertion_score == extend_right_insertion_score
              && open_left_deletion_score == extend_left_deletion_score
              && open_right_deletion_score == extend_right_deletion_score)
            algorithm = NeedlemanWunschSmithWaterman;
        else
            algorithm = Gotoh;
        self->algorithm = algorithm;
    }
    return algorithm;
}

static int
Aligner_init(Aligner *self, PyObject *args, PyObject *kwds)
{
    self->mode = Global;
    self->match = 1.0;
    self->mismatch = 0.0;
    self->epsilon = 1.e-6;
    self->open_internal_insertion_score = -1.0;
    self->extend_internal_insertion_score = -1.0;
    self->open_internal_deletion_score = -1.0;
    self->extend_internal_deletion_score = -1.0;
    self->open_left_insertion_score = -1.0;
    self->extend_left_insertion_score = -1.0;
    self->open_right_insertion_score = -1.0;
    self->extend_right_insertion_score = -1.0;
    self->open_left_deletion_score = -1.0;
    self->extend_left_deletion_score = -1.0;
    self->open_right_deletion_score = -1.0;
    self->extend_right_deletion_score = -1.0;
    self->open_internal_insertion_score_set = false;
    self->extend_internal_insertion_score_set = false;
    self->open_left_insertion_score_set = false;
    self->extend_left_insertion_score_set = false;
    self->open_right_insertion_score_set = false;
    self->extend_right_insertion_score_set = false;
    self->open_internal_deletion_score_set = false;
    self->extend_internal_deletion_score_set = false;
    self->open_left_deletion_score_set = false;
    self->extend_left_deletion_score_set = false;
    self->open_right_deletion_score_set = false;
    self->extend_right_deletion_score_set = false;
    self->insertion_score_function = NULL;
    self->deletion_score_function = NULL;
    self->substitution_matrix.obj = NULL;
    self->substitution_matrix.buf = NULL;
    self->algorithm = Unknown;
    self->alphabet = NULL;
    self->wildcard = -1;
    return 0;
}

static void
Aligner_dealloc(Aligner* self)
{   Py_XDECREF(self->insertion_score_function);
    Py_XDECREF(self->deletion_score_function);
    PyBuffer_Release(&self->substitution_matrix);
    Py_XDECREF(self->alphabet);
    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, or Waterman-Smith-Beyer global or local "
      "alignment algorithm, or the Fast Optimal Global Sequence Alignment "
      "Algorithm";
  return PyUnicode_FromString(text);
}

static PyObject*
Aligner_str(Aligner* self)
{
    Py_uintptr_t id;
    char text[1024];
    char* p = text;
    char* value;
    PyObject* substitution_matrix = self->substitution_matrix.obj;
    void* args[3];
    int n = 0;
    PyObject* wildcard = NULL;
    PyObject* s = NULL;

    p += sprintf(p, "Pairwise sequence aligner with parameters\n");
    if (substitution_matrix) {
#ifdef PYPY_VERSION
        // For PyPy, use PyObject_CallFunction to get id(self)
        PyObject* builtins = PyEval_GetBuiltins();
        PyObject* id_func = PyDict_GetItemString(builtins, "id");
        PyObject* id_result = PyObject_CallFunctionObjArgs(id_func,
                                                           substitution_matrix,
                                                           NULL);
        if (id_result) {
            if (PyLong_Check(id_result)) {
                id = (Py_uintptr_t)PyLong_AsUnsignedLongLong(id_result);
            }
            Py_DECREF(id_result);
        }
#else
        // In CPython, id(self) is just the address
        id = (Py_uintptr_t)substitution_matrix;
#endif
        p += sprintf(p, "  substitution_matrix: <%s object at 0x%" PRIxPTR ">\n",
                     Py_TYPE(substitution_matrix)->tp_name, id);
    } else {
        if (self->wildcard == -1) {
            p += sprintf(p, "  wildcard: None\n");
        }
        else {
            wildcard = PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND,
                                                 &self->wildcard, 1);
            if (!wildcard) return NULL;
            p += sprintf(p, "  wildcard: '%%U'\n");
            args[n++] = wildcard;
        }
        /* Use PyOS_double_to_string to ensure that the locale does
         * not change the decimal point into a comma.
         */
        value = PyOS_double_to_string(self->match, 'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  match_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->mismatch, 'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  mismatch_score: %s\n", value);
        PyMem_Free(value);
    }
    if (self->insertion_score_function) {
        p += sprintf(p, "  insertion_score_function: %%R\n");
        args[n++] = self->insertion_score_function;
    }
    else {
        value = PyOS_double_to_string(self->open_internal_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_internal_insertion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_internal_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  extend_internal_insertion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->open_left_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_left_insertion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_left_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  extend_left_insertion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->open_right_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_right_insertion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_right_insertion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  extend_right_insertion_score: %s\n", value);
        PyMem_Free(value);
    }
    if (self->deletion_score_function) {
        p += sprintf(p, "  deletion_score_function: %%R\n");
        args[n++] = self->deletion_score_function;
    }
    else {
        value = PyOS_double_to_string(self->open_internal_deletion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_internal_deletion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_internal_deletion_score,
                                      'f', 6, 0, NULL);
        p += sprintf(p, "  extend_internal_deletion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->open_left_deletion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_left_deletion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_left_deletion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  extend_left_deletion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->open_right_deletion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  open_right_deletion_score: %s\n", value);
        PyMem_Free(value);
        value = PyOS_double_to_string(self->extend_right_deletion_score,
                                      'f', 6, 0, NULL);
        if (!value) goto exit;
        p += sprintf(p, "  extend_right_deletion_score: %s\n", value);
        PyMem_Free(value);
    }
    switch (self->mode) {
        case Global: sprintf(p, "  mode: global\n"); break;
        case Local: sprintf(p, "  mode: local\n"); break;
        case FOGSAA_Mode: sprintf(p, "  mode: fogsaa\n"); break;
        default:
            ERR_UNEXPECTED_MODE
            return NULL;
    }
    s = PyUnicode_FromFormat(text, args[0], args[1], args[2]);

exit:
    Py_XDECREF(wildcard);
    return s;
}

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

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;
        case FOGSAA_Mode: message = "fogsaa"; break;
    }
    return PyUnicode_FromString(message);
}

static int
Aligner_set_mode(Aligner* self, PyObject* value, void* closure)
{
    self->algorithm = Unknown;
    if (PyUnicode_Check(value)) {
        if (PyUnicode_CompareWithASCIIString(value, "global") == 0) {
            self->mode = Global;
            return 0;
        }
        if (PyUnicode_CompareWithASCIIString(value, "local") == 0) {
            self->mode = Local;
            return 0;
        }
        if (PyUnicode_CompareWithASCIIString(value, "fogsaa") == 0) {
            self->mode = FOGSAA_Mode;
            return 0;
        }
    }
    PyErr_SetString(PyExc_ValueError,
                    "invalid mode (expected 'global', 'local', or 'fogsaa'");
    return -1;
}

static char Aligner_match_score__doc__[] = "match score";

static PyObject*
Aligner_get_match_score(Aligner* self, void* closure)
{   if (self->substitution_matrix.obj) {
        Py_INCREF(Py_None);
        return Py_None;
    }
    return PyFloat_FromDouble(self->match);
}

static int
Aligner_set_match_score(Aligner* self, PyObject* value, void* closure)
{
    const double match = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) {
        PyErr_SetString(PyExc_ValueError, "invalid match score");
        return -1;
    }
    PyBuffer_Release(&self->substitution_matrix);
    /* does nothing if self->substitution_matrix.obj is NULL */
    self->match = match;
    return 0;
}

static char Aligner_mismatch_score__doc__[] = "mismatch score";

static PyObject*
Aligner_get_mismatch_score(Aligner* self, void* closure)
{   if (self->substitution_matrix.obj) {
        Py_INCREF(Py_None);
        return Py_None;
    }
    return PyFloat_FromDouble(self->mismatch);
}

static int
Aligner_set_mismatch_score(Aligner* self, PyObject* value, void* closure)
{
    const double mismatch = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) {
        PyErr_SetString(PyExc_ValueError, "invalid mismatch score");
        return -1;
    }
    PyBuffer_Release(&self->substitution_matrix);
    /* does nothing if self->substitution_matrix.obj is NULL */
    self->mismatch = mismatch;
    return 0;
}

static char Aligner_substitution_matrix__doc__[] = "substitution_matrix";

static PyObject*
Aligner_get_substitution_matrix(Aligner* self, void* closure)
{   PyObject* object = self->substitution_matrix.obj;
    if (!object) object = Py_None;
    Py_INCREF(object);
    return object;
}

static int
substitution_matrix_converter(PyObject* argument, void* pointer)
{
    const int flag = PyBUF_FORMAT | PyBUF_ND;
    Py_buffer* view = pointer;
    if (argument == NULL) {
        PyBuffer_Release(view);
        return 1;
    }
    if (PyObject_GetBuffer(argument, view, flag) != 0) {
        PyErr_SetString(PyExc_ValueError, "expected a matrix");
        return 0;
    }
    if (view->ndim != 2) {
        PyErr_Format(PyExc_ValueError,
         "substitution matrix has incorrect rank (%d expected 2)",
          view->ndim);
        PyBuffer_Release(view);
        return 0;
    }
    if (view->len == 0) {
        PyErr_SetString(PyExc_ValueError, "substitution matrix has zero size");
        PyBuffer_Release(view);
        return 0;
    }
    if (strcmp(view->format, "d") != 0) {
        PyErr_SetString(PyExc_ValueError,
                "substitution matrix should contain float values");
        PyBuffer_Release(view);
        return 0;
    }
    if (view->itemsize != sizeof(double)) {
        PyErr_Format(PyExc_RuntimeError,
                    "substitution matrix has unexpected item byte size "
                    "(%zd, expected %zd)", view->itemsize, sizeof(double));
        PyBuffer_Release(view);
        return 0;
    }
    if (view->shape[0] != view->shape[1]) {
        PyErr_Format(PyExc_ValueError,
                    "substitution matrix should be square "
                    "(found a %zd x %zd matrix)",
                    view->shape[0], view->shape[1]);
        PyBuffer_Release(view);
        return 0;
    }
    return Py_CLEANUP_SUPPORTED;
}

static int
Aligner_set_substitution_matrix(Aligner* self, PyObject* values, void* closure)
{
    Py_buffer view;
    if (values == Py_None) {
        PyBuffer_Release(&self->substitution_matrix);
        return 0;
    }
    if (substitution_matrix_converter(values, &view) == 0) return -1;
    PyBuffer_Release(&self->substitution_matrix);
    self->substitution_matrix = view;
    return 0;
}

static char Aligner_gap_score__doc__[] = "gap score";

static PyObject*
Aligner_get_gap_score(Aligner* self, void* closure)
{   
    if (self->insertion_score_function || self->deletion_score_function) {
        if (self->insertion_score_function != self->deletion_score_function) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        Py_INCREF(self->insertion_score_function);
        return self->insertion_score_function;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->extend_internal_insertion_score
         || score != self->open_left_insertion_score
         || score != self->extend_left_insertion_score
         || score != self->open_right_insertion_score
         || score != self->extend_right_insertion_score
         || score != self->open_internal_deletion_score
         || score != self->extend_internal_deletion_score
         || score != self->open_left_deletion_score
         || score != self->extend_left_deletion_score
         || score != self->open_right_deletion_score
         || score != self->extend_right_deletion_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->insertion_score_function);
        Py_XDECREF(self->deletion_score_function);
        Py_INCREF(value);
        Py_INCREF(value);
        self->insertion_score_function = value;
        self->deletion_score_function = value;
    }
    else {
        const double score = PyFloat_AsDouble(value);
        if (PyErr_Occurred()) return -1;
        if (self->insertion_score_function) {
            Py_DECREF(self->insertion_score_function);
            self->insertion_score_function = NULL;
        }
        if (self->deletion_score_function) {
            Py_DECREF(self->deletion_score_function);
            self->deletion_score_function = NULL;
        }
        self->open_internal_insertion_score = score;
        self->open_internal_insertion_score_set = true;
        self->extend_internal_insertion_score = score;
        self->extend_internal_insertion_score_set = true;
        self->open_left_insertion_score = score;
        self->open_left_insertion_score_set = true;
        self->extend_left_insertion_score = score;
        self->extend_left_insertion_score_set = true;
        self->open_right_insertion_score = score;
        self->open_right_insertion_score_set = true;
        self->extend_right_insertion_score = score;
        self->extend_right_insertion_score_set = true;
        self->open_internal_deletion_score = score;
        self->open_internal_deletion_score_set = true;
        self->extend_internal_deletion_score = score;
        self->extend_internal_deletion_score_set = true;
        self->open_left_deletion_score = score;
        self->open_left_deletion_score_set = true;
        self->extend_left_deletion_score = score;
        self->extend_left_deletion_score_set = true;
        self->open_right_deletion_score = score;
        self->open_right_deletion_score_set = true;
        self->extend_right_deletion_score = score;
        self->extend_right_deletion_score_set = true;
    }
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->open_left_insertion_score
         || score != self->open_right_insertion_score
         || score != self->open_internal_deletion_score
         || score != self->open_left_deletion_score
         || score != self->open_right_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_internal_insertion_score;
        if (score != self->extend_left_insertion_score
         || score != self->extend_right_insertion_score
         || score != self->extend_internal_deletion_score
         || score != self->extend_left_deletion_score
         || score != self->extend_right_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->extend_internal_insertion_score
         || score != self->open_internal_deletion_score
         || score != self->extend_internal_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_open_internal_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->open_internal_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_internal_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_extend_internal_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_internal_insertion_score;
        if (score != self->extend_internal_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_internal_gap_score(Aligner* self, PyObject* value,
                                      void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_insertion_score;
        if (score != self->extend_left_insertion_score
         || score != self->open_right_insertion_score
         || score != self->extend_right_insertion_score
         || score != self->open_left_deletion_score
         || score != self->extend_left_deletion_score
         || score != self->open_right_deletion_score
         || score != self->extend_right_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_open_end_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_insertion_score;
        if (score != self->open_right_insertion_score
         || score != self->open_left_deletion_score
         || score != self->open_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_end_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_extend_end_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_left_insertion_score;
        if (score != self->extend_right_insertion_score
         || score != self->extend_left_deletion_score
         || score != self->extend_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_end_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_insertion_score;
        if (score != self->extend_left_insertion_score
         || score != self->open_left_deletion_score
         || score != self->extend_left_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    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->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_right_insertion_score;
        if (score != self->extend_right_insertion_score
         || score != self->open_right_deletion_score
         || score != self->extend_right_deletion_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->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_open_left_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_insertion_score;
        if (score != self->open_left_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_left_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_extend_left_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_left_insertion_score;
        if (score != self->extend_left_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_left_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_open_right_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_right_insertion_score;
        if (score != self->open_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_right_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

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

static PyObject*
Aligner_get_extend_right_gap_score(Aligner* self, void* closure)
{   if (self->insertion_score_function || self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_right_insertion_score;
        if (score != self->extend_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_right_gap_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_insertion_score__doc__[] = "open insertion score";

static PyObject*
Aligner_get_open_insertion_score(Aligner* self, void* closure)
{   if (self->insertion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->open_left_insertion_score
         || score != self->open_right_insertion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_insertion_score__doc__[] = "extend insertion score";

static PyObject*
Aligner_get_extend_insertion_score(Aligner* self, void* closure)
{   if (self->insertion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_internal_insertion_score;
        if (score != self->extend_left_insertion_score
         || score != self->extend_right_insertion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_insertion_score__doc__[] = "insertion score";

static PyObject*
Aligner_get_insertion_score(Aligner* self, void* closure)
{   if (self->insertion_score_function) {
        Py_INCREF(self->insertion_score_function);
        return self->insertion_score_function;
    }
    else {
        const double score = self->open_internal_insertion_score;
        if (score != self->extend_internal_insertion_score
         || score != self->open_left_insertion_score
         || score != self->extend_left_insertion_score
         || score != self->open_right_insertion_score
         || score != self->extend_right_insertion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_insertion_score(Aligner* self, PyObject* value, void* closure)
{
    if (PyCallable_Check(value)) {
        Py_XDECREF(self->insertion_score_function);
        Py_INCREF(value);
        self->insertion_score_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->open_internal_insertion_score = score;
        self->open_internal_insertion_score_set = true;
        self->extend_internal_insertion_score = score;
        self->extend_internal_insertion_score_set = true;
        self->open_left_insertion_score = score;
        self->open_left_insertion_score_set = true;
        self->extend_left_insertion_score = score;
        self->extend_left_insertion_score_set = true;
        self->open_right_insertion_score = score;
        self->open_right_insertion_score_set = true;
        self->extend_right_insertion_score = score;
        self->extend_right_insertion_score_set = true;
        if (self->insertion_score_function) {
            Py_DECREF(self->insertion_score_function);
            self->insertion_score_function = NULL;
        }
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_deletion_score__doc__[] = "open deletion score";

static PyObject*
Aligner_get_open_deletion_score(Aligner* self, void* closure)
{   if (self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_internal_deletion_score;
        if (score != self->open_left_deletion_score
         || score != self->open_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_open_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_deletion_score__doc__[] = "extend deletion score";

static PyObject*
Aligner_get_extend_deletion_score(Aligner* self, void* closure)
{   if (self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->extend_internal_deletion_score;
        if (score != self->extend_left_deletion_score
         || score != self->extend_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_extend_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_deletion_score__doc__[] = "deletion score";

static PyObject*
Aligner_get_deletion_score(Aligner* self, void* closure)
{   if (self->deletion_score_function) {
        Py_INCREF(self->deletion_score_function);
        return self->deletion_score_function;
    }
    else {
        const double score = self->open_internal_deletion_score;
        if (score != self->open_left_deletion_score
         || score != self->open_right_deletion_score
         || score != self->extend_internal_deletion_score
         || score != self->extend_left_deletion_score
         || score != self->extend_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_deletion_score(Aligner* self, PyObject* value, void* closure)
{   if (PyCallable_Check(value)) {
        Py_XDECREF(self->deletion_score_function);
        Py_INCREF(value);
        self->deletion_score_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->open_internal_deletion_score = score;
        self->open_internal_deletion_score_set = true;
        self->extend_internal_deletion_score = score;
        self->extend_internal_deletion_score_set = true;
        self->open_left_deletion_score = score;
        self->open_left_deletion_score_set = true;
        self->extend_left_deletion_score = score;
        self->extend_left_deletion_score_set = true;
        self->open_right_deletion_score = score;
        self->open_right_deletion_score_set = true;
        self->extend_right_deletion_score = score;
        self->extend_right_deletion_score_set = true;
        if (self->deletion_score_function) {
            Py_DECREF(self->deletion_score_function);
            self->deletion_score_function = NULL;
        }
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_internal_insertion_score__doc__[] = "open internal insertion score";

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

static int
Aligner_set_open_internal_insertion_score(Aligner* self,
                                           PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_internal_insertion_score__doc__[] = "extend internal insertion score";

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

static int
Aligner_set_extend_internal_insertion_score(Aligner* self,
                                             PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_internal_insertion_score__doc__[] = "internal insertion score";

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

static int
Aligner_set_internal_insertion_score(Aligner* self, PyObject* value,
                                     void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_insertion_score = score;
    self->open_internal_insertion_score_set = true;
    self->extend_internal_insertion_score = score;
    self->extend_internal_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_end_insertion_score__doc__[] = "end insertion score";

static PyObject*
Aligner_get_end_insertion_score(Aligner* self, void* closure)
{   if (self->insertion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_insertion_score;
        if (score != self->extend_left_insertion_score
         || score != self->open_right_insertion_score
         || score != self->extend_right_insertion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_end_insertion_score(Aligner* self, PyObject* value, void* closure) {
    const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_end_insertion_score__doc__[] = "open end insertion score";

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

static int
Aligner_set_open_end_insertion_score(Aligner* self, PyObject* value,
                                     void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_end_insertion_score__doc__[] = "extend end insertion score";

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

static int
Aligner_set_extend_end_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_left_insertion_score__doc__[] = "open left insertion score";

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

static int
Aligner_set_open_left_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_left_insertion_score__doc__[] = "extend left insertion score";

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

static int
Aligner_set_extend_left_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_left_insertion_score__doc__[] = "left insertion score";

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

static int
Aligner_set_left_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_insertion_score = score;
    self->open_left_insertion_score_set = true;
    self->extend_left_insertion_score = score;
    self->extend_left_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_right_insertion_score__doc__[] = "open right insertion score";

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

static int
Aligner_set_open_right_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_right_insertion_score__doc__[] = "extend right insertion score";

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

static int
Aligner_set_extend_right_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_right_insertion_score__doc__[] = "right insertion score";

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

static int
Aligner_set_right_insertion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_right_insertion_score = score;
    self->open_right_insertion_score_set = true;
    self->extend_right_insertion_score = score;
    self->extend_right_insertion_score_set = true;
    if (self->insertion_score_function) {
        Py_DECREF(self->insertion_score_function);
        self->insertion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_end_deletion_score__doc__[] = "end deletion score";

static PyObject*
Aligner_get_end_deletion_score(Aligner* self, void* closure)
{   if (self->deletion_score_function) {
        PyErr_SetString(PyExc_ValueError, "using a gap score function");
        return NULL;
    }
    else {
        const double score = self->open_left_deletion_score;
        if (score != self->extend_left_deletion_score
         || score != self->open_right_deletion_score
         || score != self->extend_right_deletion_score) {
            PyErr_SetString(PyExc_ValueError, "gap scores are different");
            return NULL;
        }
        return PyFloat_FromDouble(score);
    }
}

static int
Aligner_set_end_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_end_deletion_score__doc__[] = "open end deletion score";

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

static int
Aligner_set_open_end_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_end_deletion_score__doc__[] = "extend end deletion score";

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

static int
Aligner_set_extend_end_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_internal_deletion_score__doc__[] = "open internal deletion score";

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

static int
Aligner_set_open_internal_deletion_score(Aligner* self, PyObject* value,
                                          void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_internal_deletion_score__doc__[] = "extend internal deletion score";

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

static int
Aligner_set_extend_internal_deletion_score(Aligner* self, PyObject* value,
                                            void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_internal_deletion_score__doc__[] = "internal deletion score";

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

static int
Aligner_set_internal_deletion_score(Aligner* self, PyObject* value,
                                     void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_internal_deletion_score = score;
    self->open_internal_deletion_score_set = true;
    self->extend_internal_deletion_score = score;
    self->extend_internal_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_left_deletion_score__doc__[] = "open left deletion score";

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

static int
Aligner_set_open_left_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_left_deletion_score__doc__[] = "extend left deletion score";

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

static int
Aligner_set_extend_left_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_left_deletion_score__doc__[] = "left deletion score";

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

static int
Aligner_set_left_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_left_deletion_score = score;
    self->open_left_deletion_score_set = true;
    self->extend_left_deletion_score = score;
    self->extend_left_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_open_right_deletion_score__doc__[] = "open right deletion score";

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

static int
Aligner_set_open_right_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_extend_right_deletion_score__doc__[] = "extend right deletion score";

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

static int
Aligner_set_extend_right_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_function = NULL;
    }
    self->algorithm = Unknown;
    return 0;
}

static char Aligner_right_deletion_score__doc__[] = "right deletion score";

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

static int
Aligner_set_right_deletion_score(Aligner* self, PyObject* value, void* closure)
{   const double score = PyFloat_AsDouble(value);
    if (PyErr_Occurred()) return -1;
    self->open_right_deletion_score = score;
    self->open_right_deletion_score_set = true;
    self->extend_right_deletion_score = score;
    self->extend_right_deletion_score_set = true;
    if (self->deletion_score_function) {
        Py_DECREF(self->deletion_score_function);
        self->deletion_score_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 char Aligner_wildcard__doc__[] = "wildcard character";

static PyObject*
Aligner_get_wildcard(Aligner* self, void* closure)
{
    if (self->wildcard == -1) {
        Py_INCREF(Py_None);
        return Py_None;
    }
    else {
        return PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, &self->wildcard, 1);
    }
}

static int
Aligner_set_wildcard(Aligner* self, PyObject* value, void* closure)
{
    if (value == Py_None) {
        self->wildcard = -1;
        return 0;
    }
    if (!PyUnicode_Check(value)) {
        PyErr_SetString(PyExc_TypeError,
                        "wildcard should be a single character, or None");
        return -1;
    }
    if (PyUnicode_READY(value) == -1) return -1;
    if (PyUnicode_GET_LENGTH(value) != 1) {
        PyErr_SetString(PyExc_ValueError,
                        "wildcard should be a single character, or None");
        return -1;
    }
    self->wildcard = PyUnicode_READ_CHAR(value, 0);
    return 0;
}

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;
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
            break;
        case Gotoh:
            switch (mode) {
                case Global:
                    s = "Gotoh global alignment algorithm";
                    break;
                case Local:
                    s = "Gotoh local alignment algorithm";
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
            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;
                default:
                    ERR_UNEXPECTED_MODE
                    return NULL;
            }
            break;
        case FOGSAA:
            // self->mode must be FOGSAA_Mode
            s = "Fast Optimal Global Sequence Alignment Algorithm";
            break;
        case Unknown:
        default:
            break;
    }
    return PyUnicode_FromString(s);
}

static PyGetSetDef Aligner_getset[] = {
    {"mode",
        (getter)Aligner_get_mode,
        (setter)Aligner_set_mode,
        Aligner_mode__doc__, NULL},
    {"match_score",
        (getter)Aligner_get_match_score,
        (setter)Aligner_set_match_score,
        Aligner_match_score__doc__, NULL},
    {"mismatch_score",
        (getter)Aligner_get_mismatch_score,
        (setter)Aligner_set_mismatch_score,
        Aligner_mismatch_score__doc__, NULL},
    {"match", /* synonym for match_score */
        (getter)Aligner_get_match_score,
        (setter)Aligner_set_match_score,
        Aligner_match_score__doc__, NULL},
    {"mismatch", /* synonym for mismatch_score */
        (getter)Aligner_get_mismatch_score,
        (setter)Aligner_set_mismatch_score,
        Aligner_mismatch_score__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},
    {"open_internal_gap_score",
        (getter)Aligner_get_open_internal_gap_score,
        (setter)Aligner_set_open_internal_gap_score,
        Aligner_open_internal_gap_score__doc__, NULL},
    {"extend_internal_gap_score",
        (getter)Aligner_get_extend_internal_gap_score,
        (setter)Aligner_set_extend_internal_gap_score,
        Aligner_extend_internal_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},
    {"open_end_gap_score",
        (getter)Aligner_get_open_end_gap_score,
        (setter)Aligner_set_open_end_gap_score,
        Aligner_open_end_gap_score__doc__, NULL},
    {"extend_end_gap_score",
        (getter)Aligner_get_extend_end_gap_score,
        (setter)Aligner_set_extend_end_gap_score,
        Aligner_extend_end_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},
    {"open_left_gap_score",
        (getter)Aligner_get_open_left_gap_score,
        (setter)Aligner_set_open_left_gap_score,
        Aligner_open_left_gap_score__doc__, NULL},
    {"extend_left_gap_score",
        (getter)Aligner_get_extend_left_gap_score,
        (setter)Aligner_set_extend_left_gap_score,
        Aligner_extend_left_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},
    {"open_right_gap_score",
        (getter)Aligner_get_open_right_gap_score,
        (setter)Aligner_set_open_right_gap_score,
        Aligner_open_right_gap_score__doc__, NULL},
    {"extend_right_gap_score",
        (getter)Aligner_get_extend_right_gap_score,
        (setter)Aligner_set_extend_right_gap_score,
        Aligner_extend_right_gap_score__doc__, NULL},
    {"open_insertion_score",
        (getter)Aligner_get_open_insertion_score,
        (setter)Aligner_set_open_insertion_score,
        Aligner_open_insertion_score__doc__, NULL},
    {"extend_insertion_score",
        (getter)Aligner_get_extend_insertion_score,
        (setter)Aligner_set_extend_insertion_score,
        Aligner_extend_insertion_score__doc__, NULL},
    {"insertion_score",
        (getter)Aligner_get_insertion_score,
        (setter)Aligner_set_insertion_score,
        Aligner_insertion_score__doc__, NULL},
    {"open_deletion_score",
        (getter)Aligner_get_open_deletion_score,
        (setter)Aligner_set_open_deletion_score,
        Aligner_open_deletion_score__doc__, NULL},
    {"extend_deletion_score",
        (getter)Aligner_get_extend_deletion_score,
        (setter)Aligner_set_extend_deletion_score,
        Aligner_extend_deletion_score__doc__, NULL},
    {"deletion_score",
        (getter)Aligner_get_deletion_score,
        (setter)Aligner_set_deletion_score,
        Aligner_deletion_score__doc__, NULL},
    {"end_insertion_score",
        (getter)Aligner_get_end_insertion_score,
        (setter)Aligner_set_end_insertion_score,
        Aligner_end_insertion_score__doc__, NULL},
    {"open_end_insertion_score",
        (getter)Aligner_get_open_end_insertion_score,
        (setter)Aligner_set_open_end_insertion_score,
        Aligner_open_end_insertion_score__doc__, NULL},
    {"extend_end_insertion_score",
        (getter)Aligner_get_extend_end_insertion_score,
        (setter)Aligner_set_extend_end_insertion_score,
        Aligner_extend_end_insertion_score__doc__, NULL},
    {"open_internal_insertion_score",
        (getter)Aligner_get_open_internal_insertion_score,
        (setter)Aligner_set_open_internal_insertion_score,
        Aligner_open_internal_insertion_score__doc__, NULL},
    {"extend_internal_insertion_score",
        (getter)Aligner_get_extend_internal_insertion_score,
        (setter)Aligner_set_extend_internal_insertion_score,
        Aligner_extend_internal_insertion_score__doc__, NULL},
    {"internal_insertion_score",
        (getter)Aligner_get_internal_insertion_score,
        (setter)Aligner_set_internal_insertion_score,
        Aligner_internal_insertion_score__doc__, NULL},
    {"open_left_insertion_score",
        (getter)Aligner_get_open_left_insertion_score,
        (setter)Aligner_set_open_left_insertion_score,
        Aligner_open_left_insertion_score__doc__, NULL},
    {"extend_left_insertion_score",
        (getter)Aligner_get_extend_left_insertion_score,
        (setter)Aligner_set_extend_left_insertion_score,
        Aligner_extend_left_insertion_score__doc__, NULL},
    {"left_insertion_score",
        (getter)Aligner_get_left_insertion_score,
        (setter)Aligner_set_left_insertion_score,
        Aligner_left_insertion_score__doc__, NULL},
    {"open_right_insertion_score",
        (getter)Aligner_get_open_right_insertion_score,
        (setter)Aligner_set_open_right_insertion_score,
        Aligner_open_right_insertion_score__doc__, NULL},
    {"extend_right_insertion_score",
        (getter)Aligner_get_extend_right_insertion_score,
        (setter)Aligner_set_extend_right_insertion_score,
        Aligner_extend_right_insertion_score__doc__, NULL},
    {"right_insertion_score",
        (getter)Aligner_get_right_insertion_score,
        (setter)Aligner_set_right_insertion_score,
        Aligner_right_insertion_score__doc__, NULL},
    {"end_deletion_score",
        (getter)Aligner_get_end_deletion_score,
        (setter)Aligner_set_end_deletion_score,
        Aligner_end_deletion_score__doc__, NULL},
    {"open_end_deletion_score",
        (getter)Aligner_get_open_end_deletion_score,
        (setter)Aligner_set_open_end_deletion_score,
        Aligner_open_end_deletion_score__doc__, NULL},
    {"extend_end_deletion_score",
        (getter)Aligner_get_extend_end_deletion_score,
        (setter)Aligner_set_extend_end_deletion_score,
        Aligner_extend_end_deletion_score__doc__, NULL},
    {"open_internal_deletion_score",
        (getter)Aligner_get_open_internal_deletion_score,
        (setter)Aligner_set_open_internal_deletion_score,
        Aligner_open_internal_deletion_score__doc__, NULL},
    {"extend_internal_deletion_score",
        (getter)Aligner_get_extend_internal_deletion_score,
        (setter)Aligner_set_extend_internal_deletion_score,
        Aligner_extend_internal_deletion_score__doc__, NULL},
    {"internal_deletion_score",
        (getter)Aligner_get_internal_deletion_score,
        (setter)Aligner_set_internal_deletion_score,
        Aligner_internal_deletion_score__doc__, NULL},
    {"open_left_deletion_score",
        (getter)Aligner_get_open_left_deletion_score,
        (setter)Aligner_set_open_left_deletion_score,
        Aligner_open_left_deletion_score__doc__, NULL},
    {"extend_left_deletion_score",
        (getter)Aligner_get_extend_left_deletion_score,
        (setter)Aligner_set_extend_left_deletion_score,
        Aligner_extend_left_deletion_score__doc__, NULL},
    {"left_deletion_score",
        (getter)Aligner_get_left_deletion_score,
        (setter)Aligner_set_left_deletion_score,
         Aligner_left_deletion_score__doc__, NULL},
    {"open_right_deletion_score",
        (getter)Aligner_get_open_right_deletion_score,
        (setter)Aligner_set_open_right_deletion_score,
        Aligner_open_right_deletion_score__doc__, NULL},
    {"extend_right_deletion_score",
        (getter)Aligner_get_extend_right_deletion_score,
        (setter)Aligner_set_extend_right_deletion_score,
        Aligner_extend_right_deletion_score__doc__, NULL},
    {"right_deletion_score",
        (getter)Aligner_get_right_deletion_score,
        (setter)Aligner_set_right_deletion_score,
        Aligner_right_deletion_score__doc__, NULL},
    {"epsilon",
        (getter)Aligner_get_epsilon,
        (setter)Aligner_set_epsilon,
        Aligner_epsilon__doc__, NULL},
    {"wildcard",
        (getter)Aligner_get_wildcard,
        (setter)Aligner_set_wildcard,
        Aligner_wildcard__doc__, NULL},
    {"algorithm",
        (getter)Aligner_get_algorithm,
        (setter)NULL,
        Aligner_algorithm__doc__, NULL},
    {NULL, NULL, 0, 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, align_score) \
    score = temp + (align_score); \
    trace = DIAGONAL; \
    temp = row[j-1] + hgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = HORIZONTAL; \
    } \
    else if (temp > score - epsilon) trace |= HORIZONTAL; \
    temp = row[j] + vgap; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = VERTICAL; \
    } \
    else if (temp > score - epsilon) trace |= VERTICAL; \
    temp = row[j]; \
    row[j] = score; \
    M[i][j].trace = trace;

#define SELECT_TRACE_SMITH_WATERMAN_HVD(align_score) \
    trace = DIAGONAL; \
    score = temp + (align_score); \
    temp = row[j-1] + gap_extend_A; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = HORIZONTAL; \
    } \
    else if (temp > score - epsilon) trace |= HORIZONTAL; \
    temp = row[j] + gap_extend_B; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = VERTICAL; \
    } \
    else if (temp > score - epsilon) trace |= VERTICAL; \
    if (score < epsilon) { \
        score = 0; \
        trace = STARTPOINT; \
    } \
    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 = row[j]; \
    row[j] = score;

#define SELECT_TRACE_SMITH_WATERMAN_D(align_score) \
    score = temp + (align_score); \
    trace = DIAGONAL; \
    if (score < epsilon) { \
        score = 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 = row[j]; \
    row[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(align_score) \
    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 += (align_score); \
    if (score < epsilon) { \
        score = 0; \
        trace = STARTPOINT; \
    } \
    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_row[i-1][j-1]; \
    temp = Ix_row[i-1][j-1]; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Ix_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Ix_MATRIX; \
    temp = Iy_row[i-1][j-1]; \
    if (temp > score + epsilon) { \
        score = temp; \
        trace = Iy_MATRIX; \
    } \
    else if (temp > score - epsilon) trace |= Iy_MATRIX; \
    M_row[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 = STARTPOINT; \
    } \
    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_row[i][j] = score; \
    M[i][j].trace = trace;

struct fogsaa_cell {
    double present_score, lower, upper;
    int type, filled, is_left_gap;
};

struct fogsaa_queue {
    struct fogsaa_queue_node *array;
    int size, capacity;
};

struct fogsaa_queue_node {
    int pA, pB, type_upto_next, next_type;
    double next_lower, next_upper;
};

#define MATRIX(a, b) matrix[a * (nB+1) + b]

#define FOGSAA_SORT() \
  for (i = 0; i < 2; i++) { \
    for (j = 0; j < 2 - i; j++) { \
      if ((child_lbounds[j] < child_lbounds[j + 1]) || ((child_lbounds[j] == child_lbounds[j + 1]) && (child_ubounds[j] < child_ubounds[j + 1]))) { \
        t = child_lbounds[j]; \
        child_lbounds[j] = child_lbounds[j + 1]; \
        child_lbounds[j + 1] = t; \
        \
        t = child_types[j]; \
        child_types[j] = child_types[j + 1]; \
        child_types[j + 1] = t; \
        \
        t = child_ubounds[j]; \
        child_ubounds[j] = child_ubounds[j + 1]; \
        child_ubounds[j + 1] = t; \
      } \
    } \
  }

/* This doesn't always work if the gap score is less than the mismatch score */
#define FOGSAA_CALCULATE_SCORE(curr_score, curr_type, lower, upper, pA, pB) \
    if (nA - (pA) <= nB - (pB)) { \
        if (pA == nA && (curr_type) == HORIZONTAL) { \
            /* If we're already at the end and a gap is already open */ \
            lower = curr_score + right_gap_extend_A * (nB - (pB)); \
            upper = curr_score + right_gap_extend_A * (nB - (pB)); \
        } else { \
            lower = curr_score + (nA - (pA)) * mismatch; \
            upper = curr_score + (nA - (pA)) * match; \
            t = right_gap_open_A + right_gap_extend_A * ((nB - (pB)) - (nA - (pA)) - 1); \
            t2 =  gap_extend_A * ((nB - (pB)) - (nA - (pA))); \
            if ((curr_type) == HORIZONTAL && t2 > t) { \
                /* if we already have a gap open, then we can just extend */ \
                /* from the open gap and match/mismatch later. we don't */ \
                /* need to open a new one */ \
                lower += t2; \
                upper += t2; \
            } else { \
                lower += t; \
                upper += t; \
            } \
        } \
    } else { \
        if (pB == nB && (curr_type) == VERTICAL) { \
            /* If we're already at the end and a gap is already open */ \
            lower = curr_score + right_gap_extend_B * (nA - (pA)); \
            upper = curr_score + right_gap_extend_B * (nA - (pA)); \
        } else { \
            lower = curr_score + (nB - (pB)) * mismatch; \
            upper = curr_score + (nB - (pB)) * match; \
            t = right_gap_open_B + right_gap_extend_B * ((nA - (pA)) - (nB - (pB)) - 1); \
            t2 =  gap_extend_B * ((nA - (pA)) - (nB - (pB))); \
            if ((curr_type) == VERTICAL && t2 > t) { \
                /* if we already have a gap open, then we can just extend */ \
                /* from the open gap and match/mismatch later. we don't */ \
                /* need to open a new one */ \
                lower += t2; \
                upper += t2; \
            } else { \
                lower += t; \
                upper += t; \
            } \
        } \
    }

// node has higher priority if upper bound is higher, or if upper bounds are
// equal, if lower bound is higher
#define FOGSAA_QUEUE_HEAP_COND(a, b) \
    (queue->array[a].next_upper > queue->array[b].next_upper || \
     (queue->array[a].next_upper == queue->array[b].next_upper && \
      queue->array[a].next_lower > queue->array[b].next_lower))

static int
fogsaa_queue_insert(struct fogsaa_queue *queue, int pA, int pB,
        int type_total, int next_type, double next_lower, double next_upper) {
    // max heap implementation for the priority queue by next_upper
    struct fogsaa_queue_node temp;
    int i;

    if (queue->size + 1 >= queue->capacity) {
        struct fogsaa_queue_node *old_array = queue->array;
        queue->array = PyMem_Realloc(queue->array,
                sizeof(struct fogsaa_queue_node) * (queue->capacity + 1) * 2);
        if (queue->array == NULL) {
            PyMem_Free(old_array);
            return 0; // caller should return PyErr_NoMemory();
        }
        queue->capacity = (queue->capacity + 1) * 2;
    }

    i = queue->size;
    queue->array[i].pA = pA;
    queue->array[i].pB = pB;
    queue->array[i].next_type = next_type;
    queue->array[i].next_lower = next_lower;
    queue->array[i].type_upto_next = type_total;
    queue->array[i].next_upper = next_upper;

    while (i != 0 && !FOGSAA_QUEUE_HEAP_COND((i-1)/2, i)) {
        // swap the child and the smaller parent
        temp = queue->array[i];
        queue->array[i] = queue->array[(i-1)/2];
        queue->array[(i-1)/2] = temp;
        i = (i-1)/2;
    }
    queue->size += 1;
    return 1;
}

static struct fogsaa_queue_node fogsaa_queue_pop(struct fogsaa_queue *queue) {
    // caller code must check queue is not empty
    struct fogsaa_queue_node temp, root = queue->array[0];
    int largest_child, i = 0;
    queue->size -= 1;
    queue->array[i] = queue->array[queue->size];
    while (1) {
        largest_child = i;
        if (2*i+1 < queue->size && !FOGSAA_QUEUE_HEAP_COND(i, 2*i+1))
            largest_child = 2*i+1;
        if (2*i+2 < queue->size && !FOGSAA_QUEUE_HEAP_COND(largest_child, 2*i+2))
            largest_child = 2*i+2;
        if (largest_child != i) {
            // swap the parent and the larger child
            temp = queue->array[i];
            queue->array[i] = queue->array[largest_child];
            queue->array[largest_child] = temp;
            i = largest_child;
        } else {
            break;
        }
    }
    return root;
}

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

#define NEEDLEMANWUNSCH_SCORE(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    double score; \
    double temp; \
    double* row; \
    double left_gap_extend_A; \
    double right_gap_extend_A; \
    double left_gap_extend_B; \
    double right_gap_extend_B; \
    switch (strand) { \
        case '+': \
            left_gap_extend_A = self->extend_left_insertion_score; \
            right_gap_extend_A = self->extend_right_insertion_score; \
            left_gap_extend_B = self->extend_left_deletion_score; \
            right_gap_extend_B = self->extend_right_deletion_score; \
            break; \
        case '-': \
            left_gap_extend_A = self->extend_right_insertion_score; \
            right_gap_extend_A = self->extend_left_insertion_score; \
            left_gap_extend_B = self->extend_right_deletion_score; \
            right_gap_extend_B = self->extend_left_deletion_score; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "strand was neither '+' nor '-'"); \
            return NULL; \
    } \
\
    /* Needleman-Wunsch algorithm */ \
    row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!row) return PyErr_NoMemory(); \
\
    /* The top row of the score matrix is a special case, \
     * as there are no previously aligned characters. \
     */ \
    row[0] = 0.0; \
    for (j = 1; j <= nB; j++) row[j] = j * left_gap_extend_A; \
    for (i = 1; i < nA; i++) { \
        kA = sA[i-1]; \
        temp = row[0]; \
        row[0] = i * left_gap_extend_B; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_SCORE_GLOBAL(temp + (align_score), \
                                row[j] + gap_extend_B, \
                                row[j-1] + gap_extend_A); \
            temp = row[j]; \
            row[j] = score; \
        } \
        kB = sB[nB-1]; \
        SELECT_SCORE_GLOBAL(temp + (align_score), \
                            row[nB] + right_gap_extend_B, \
                            row[nB-1] + gap_extend_A); \
        temp = row[nB]; \
        row[nB] = score; \
    } \
    kA = sA[nA-1]; \
    temp = row[0]; \
    row[0] = nA * right_gap_extend_B; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_SCORE_GLOBAL(temp + (align_score), \
                            row[j] + gap_extend_B, \
                            row[j-1] + right_gap_extend_A); \
        temp = row[j]; \
        row[j] = score; \
    } \
    kB = sB[nB-1]; \
    SELECT_SCORE_GLOBAL(temp + (align_score), \
                        row[nB] + right_gap_extend_B, \
                        row[nB-1] + right_gap_extend_A); \
    PyMem_Free(row); \
    return PyFloat_FromDouble(score);


#define SMITHWATERMAN_SCORE(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    double score; \
    double* row; \
    double temp; \
    double maximum = 0; \
\
    /* Smith-Waterman algorithm */ \
    row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!row) 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++) \
        row[j] = 0; \
    for (i = 1; i < nA; i++) { \
        kA = sA[i-1]; \
        temp = 0; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_SCORE_LOCAL3(temp + (align_score), \
                                row[j] + gap_extend_B, \
                                row[j-1] + gap_extend_A); \
            temp = row[j]; \
            row[j] = score; \
        } \
        kB = sB[nB-1]; \
        SELECT_SCORE_LOCAL1(temp + (align_score)); \
        temp = row[nB]; \
        row[nB] = score; \
    } \
    kA = sA[nA-1]; \
    temp = 0; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_SCORE_LOCAL1(temp + (align_score)); \
        temp = row[j]; \
        row[j] = score; \
    } \
    kB = sB[nB-1]; \
    SELECT_SCORE_LOCAL1(temp + (align_score)); \
    PyMem_Free(row); \
    return PyFloat_FromDouble(maximum);


#define NEEDLEMANWUNSCH_ALIGN(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    const double epsilon = self->epsilon; \
    Trace** M; \
    double score; \
    int trace; \
    double temp; \
    double* row = NULL; \
    PathGenerator* paths; \
    double left_gap_extend_A; \
    double right_gap_extend_A; \
    double left_gap_extend_B; \
    double right_gap_extend_B; \
    switch (strand) { \
        case '+': \
            left_gap_extend_A = self->extend_left_insertion_score; \
            right_gap_extend_A = self->extend_right_insertion_score; \
            left_gap_extend_B = self->extend_left_deletion_score; \
            right_gap_extend_B = self->extend_right_deletion_score; \
            break; \
        case '-': \
            left_gap_extend_A = self->extend_right_insertion_score; \
            right_gap_extend_A = self->extend_left_insertion_score; \
            left_gap_extend_B = self->extend_right_deletion_score; \
            right_gap_extend_B = self->extend_left_deletion_score; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "strand was neither '+' nor '-'"); \
            return NULL; \
    } \
\
    /* Needleman-Wunsch algorithm */ \
    paths = PathGenerator_create_NWSW(nA, nB, Global, strand); \
    if (!paths) return NULL; \
    row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!row) { \
        Py_DECREF(paths); \
        return PyErr_NoMemory(); \
    } \
    M = paths->M; \
    row[0] = 0; \
    for (j = 1; j <= nB; j++) row[j] = j * left_gap_extend_A; \
    for (i = 1; i < nA; i++) { \
        temp = row[0]; \
        row[0] = i * left_gap_extend_B; \
        kA = sA[i-1]; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_TRACE_NEEDLEMAN_WUNSCH(gap_extend_A, gap_extend_B, align_score); \
        } \
        kB = sB[j-1]; \
        SELECT_TRACE_NEEDLEMAN_WUNSCH(gap_extend_A, right_gap_extend_B, align_score); \
    } \
    temp = row[0]; \
    row[0] = i * left_gap_extend_B; \
    kA = sA[nA-1]; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_TRACE_NEEDLEMAN_WUNSCH(right_gap_extend_A, gap_extend_B, align_score); \
    } \
    kB = sB[j-1]; \
    SELECT_TRACE_NEEDLEMAN_WUNSCH(right_gap_extend_A, right_gap_extend_B, align_score); \
    PyMem_Free(row); \
    M[nA][nB].path = 0; \
    return Py_BuildValue("fN", score, paths);


#define SMITHWATERMAN_ALIGN(align_score) \
    int i; \
    int j; \
    int im = nA; \
    int jm = nB; \
    int kA; \
    int kB; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    const double epsilon = self->epsilon; \
    Trace** M = NULL; \
    double maximum = 0; \
    double score = 0; \
    double* row = NULL; \
    double temp; \
    int trace; \
    PathGenerator* paths = NULL; \
\
    /* Smith-Waterman algorithm */ \
    paths = PathGenerator_create_NWSW(nA, nB, Local, strand); \
    if (!paths) return NULL; \
    row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!row) { \
        Py_DECREF(paths); \
        return PyErr_NoMemory(); \
    } \
    M = paths->M; \
    for (j = 0; j <= nB; j++) row[j] = 0; \
    for (i = 1; i < nA; i++) { \
        temp = 0; \
        kA = sA[i-1]; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_TRACE_SMITH_WATERMAN_HVD(align_score); \
        } \
        kB = sB[nB-1]; \
        SELECT_TRACE_SMITH_WATERMAN_D(align_score); \
    } \
    temp = 0; \
    kA = sA[nA-1]; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_TRACE_SMITH_WATERMAN_D(align_score); \
    } \
    kB = sB[nB-1]; \
    SELECT_TRACE_SMITH_WATERMAN_D(align_score); \
    PyMem_Free(row); \
\
    /* As we don't allow zero-score extensions to alignments, \
     * we need to remove all traces towards an ENDPOINT. \
     * In addition, some points then won't have any path to a STARTPOINT. \
     * Here, use path as a temporary variable to indicate if the point \
     * is reachable from a STARTPOINT. If it is unreachable, remove all \
     * traces from it, and don't allow it to be an ENDPOINT. It may still \
     * be a valid STARTPOINT. */ \
    for (j = 0; j <= nB; j++) M[0][j].path = 1; \
    for (i = 1; i <= nA; i++) { \
        M[i][0].path = 1; \
        for (j = 1; j <= nB; j++) { \
            trace = M[i][j].trace; \
            /* Remove traces to unreachable points. */ \
            if (!M[i-1][j-1].path) trace &= ~DIAGONAL; \
            if (!M[i][j-1].path) trace &= ~HORIZONTAL; \
            if (!M[i-1][j].path) trace &= ~VERTICAL; \
            if (trace & (STARTPOINT | HORIZONTAL | VERTICAL | DIAGONAL)) { \
                /* The point is reachable. */ \
                if (trace & ENDPOINT) M[i][j].path = 0; /* no extensions after ENDPOINT */ \
                else M[i][j].path = 1; \
            } \
            else { \
                /* The point is not reachable. Then it is not a STARTPOINT, \
                 * all traces from it can be removed, and it cannot act as \
                 * an ENDPOINT. */ \
                M[i][j].path = 0; \
                trace = 0; \
            } \
            M[i][j].trace = trace; \
        } \
    } \
    if (maximum == 0) M[0][0].path = NONE; \
    else M[0][0].path = 0; \
    return Py_BuildValue("fN", maximum, paths);


#define GOTOH_GLOBAL_SCORE(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_open_A = self->open_internal_insertion_score; \
    const double gap_open_B = self->open_internal_deletion_score; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    double left_gap_open_A; \
    double left_gap_open_B; \
    double left_gap_extend_A; \
    double left_gap_extend_B; \
    double right_gap_open_A; \
    double right_gap_open_B; \
    double right_gap_extend_A; \
    double right_gap_extend_B; \
    double* M_row = NULL; \
    double* Ix_row = NULL; \
    double* Iy_row = NULL; \
    double score; \
    double temp; \
    double M_temp; \
    double Ix_temp; \
    double Iy_temp; \
    switch (strand) { \
        case '+': \
            left_gap_open_A = self->open_left_insertion_score; \
            left_gap_open_B = self->open_left_deletion_score; \
            left_gap_extend_A = self->extend_left_insertion_score; \
            left_gap_extend_B = self->extend_left_deletion_score; \
            right_gap_open_A = self->open_right_insertion_score; \
            right_gap_open_B = self->open_right_deletion_score; \
            right_gap_extend_A = self->extend_right_insertion_score; \
            right_gap_extend_B = self->extend_right_deletion_score; \
            break; \
        case '-': \
            left_gap_open_A = self->open_right_insertion_score; \
            left_gap_open_B = self->open_right_deletion_score; \
            left_gap_extend_A = self->extend_right_insertion_score; \
            left_gap_extend_B = self->extend_right_deletion_score; \
            right_gap_open_A = self->open_left_insertion_score; \
            right_gap_open_B = self->open_left_deletion_score; \
            right_gap_extend_A = self->extend_left_insertion_score; \
            right_gap_extend_B = self->extend_left_deletion_score; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "strand was neither '+' nor '-'"); \
            return NULL; \
    } \
\
    /* Gotoh algorithm with three states */ \
    M_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!M_row) goto exit; \
    Ix_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Ix_row) goto exit; \
    Iy_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Iy_row) goto exit; \
\
    /* The top row of the score matrix is a special case, \
     * as there are no previously aligned characters. \
     */ \
    M_row[0] = 0; \
    Ix_row[0] = -DBL_MAX; \
    Iy_row[0] = -DBL_MAX; \
    for (j = 1; j <= nB; j++) { \
        M_row[j] = -DBL_MAX; \
        Ix_row[j] = -DBL_MAX; \
        Iy_row[j] = left_gap_open_A + left_gap_extend_A * (j-1); \
    } \
\
    for (i = 1; i < nA; i++) { \
        M_temp = M_row[0]; \
        Ix_temp = Ix_row[0]; \
        Iy_temp = Iy_row[0]; \
        M_row[0] = -DBL_MAX; \
        Ix_row[0] = left_gap_open_B + left_gap_extend_B * (i-1); \
        Iy_row[0] = -DBL_MAX; \
        kA = sA[i-1]; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_SCORE_GLOBAL(M_temp, \
                                Ix_temp, \
                                Iy_temp); \
            M_temp = M_row[j]; \
            M_row[j] = score + (align_score); \
            SELECT_SCORE_GLOBAL(M_temp + gap_open_B, \
                                Ix_row[j] + gap_extend_B, \
                                Iy_row[j] + gap_open_B); \
            Ix_temp = Ix_row[j]; \
            Ix_row[j] = score; \
            SELECT_SCORE_GLOBAL(M_row[j-1] + gap_open_A, \
                                Ix_row[j-1] + gap_open_A, \
                                Iy_row[j-1] + gap_extend_A); \
            Iy_temp = Iy_row[j]; \
            Iy_row[j] = score; \
        } \
        kB = sB[nB-1]; \
        SELECT_SCORE_GLOBAL(M_temp, \
                            Ix_temp, \
                            Iy_temp); \
        M_temp = M_row[nB]; \
        M_row[nB] = score + (align_score); \
        SELECT_SCORE_GLOBAL(M_temp + right_gap_open_B, \
                            Ix_row[nB] + right_gap_extend_B, \
                            Iy_row[nB] + right_gap_open_B); \
        Ix_row[nB] = score; \
        SELECT_SCORE_GLOBAL(M_row[nB-1] + gap_open_A, \
                            Iy_row[nB-1] + gap_extend_A, \
                            Ix_row[nB-1] + gap_open_A); \
        Iy_row[nB] = score; \
    } \
\
    M_temp = M_row[0]; \
    Ix_temp = Ix_row[0]; \
    Iy_temp = Iy_row[0]; \
    M_row[0] = -DBL_MAX; \
    Ix_row[0] = left_gap_open_B + left_gap_extend_B * (i-1); \
    Iy_row[0] = -DBL_MAX; \
    kA = sA[nA-1]; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_SCORE_GLOBAL(M_temp, \
                            Ix_temp, \
                            Iy_temp); \
        M_temp = M_row[j]; \
        M_row[j] = score + (align_score); \
        SELECT_SCORE_GLOBAL(M_temp + gap_open_B, \
                            Ix_row[j] + gap_extend_B, \
                            Iy_row[j] + gap_open_B); \
        Ix_temp = Ix_row[j]; \
        Ix_row[j] = score; \
        SELECT_SCORE_GLOBAL(M_row[j-1] + right_gap_open_A, \
                            Iy_row[j-1] + right_gap_extend_A, \
                            Ix_row[j-1] + right_gap_open_A); \
        Iy_temp = Iy_row[j]; \
        Iy_row[j] = score; \
    } \
\
    kB = sB[nB-1]; \
    SELECT_SCORE_GLOBAL(M_temp, \
                        Ix_temp, \
                        Iy_temp); \
    M_temp = M_row[nB]; \
    M_row[nB] = score + (align_score); \
    SELECT_SCORE_GLOBAL(M_temp + right_gap_open_B, \
                        Ix_row[nB] + right_gap_extend_B, \
                        Iy_row[nB] + right_gap_open_B); \
    Ix_temp = Ix_row[nB]; \
    Ix_row[nB] = score; \
    SELECT_SCORE_GLOBAL(M_row[nB-1] + right_gap_open_A, \
                        Ix_row[nB-1] + right_gap_open_A, \
                        Iy_row[nB-1] + right_gap_extend_A); \
    Iy_temp = Iy_row[nB]; \
    Iy_row[nB] = score; \
\
    SELECT_SCORE_GLOBAL(M_row[nB], Ix_row[nB], Iy_row[nB]); \
    PyMem_Free(M_row); \
    PyMem_Free(Ix_row); \
    PyMem_Free(Iy_row); \
    return PyFloat_FromDouble(score); \
\
exit: \
    if (M_row) PyMem_Free(M_row); \
    if (Ix_row) PyMem_Free(Ix_row); \
    if (Iy_row) PyMem_Free(Iy_row); \
    return PyErr_NoMemory(); \


#define GOTOH_LOCAL_SCORE(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_open_A = self->open_internal_insertion_score; \
    const double gap_open_B = self->open_internal_deletion_score; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    double* M_row = NULL; \
    double* Ix_row = NULL; \
    double* Iy_row = NULL; \
    double score; \
    double temp; \
    double M_temp; \
    double Ix_temp; \
    double Iy_temp; \
    double maximum = 0.0; \
\
    /* Gotoh algorithm with three states */ \
    M_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!M_row) goto exit; \
    Ix_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Ix_row) goto exit; \
    Iy_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Iy_row) goto exit; \
 \
    /* The top row of the score matrix is a special case, \
     * as there are no previously aligned characters. \
     */ \
    M_row[0] = 0; \
    Ix_row[0] = -DBL_MAX; \
    Iy_row[0] = -DBL_MAX; \
    for (j = 1; j <= nB; j++) { \
        M_row[j] = -DBL_MAX; \
        Ix_row[j] = -DBL_MAX; \
        Iy_row[j] = 0; \
    } \
    for (i = 1; i < nA; i++) { \
        M_temp = M_row[0]; \
        Ix_temp = Ix_row[0]; \
        Iy_temp = Iy_row[0]; \
        M_row[0] = -DBL_MAX; \
        Ix_row[0] = 0; \
        Iy_row[0] = -DBL_MAX; \
        kA = sA[i-1]; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp, \
                                           Ix_temp, \
                                           Iy_temp, \
                                           (align_score)); \
            M_temp = M_row[j]; \
            M_row[j] = score; \
            SELECT_SCORE_LOCAL3(M_temp + gap_open_B, \
                                Ix_row[j] + gap_extend_B, \
                                Iy_row[j] + gap_open_B); \
            Ix_temp = Ix_row[j]; \
            Ix_row[j] = score; \
            SELECT_SCORE_LOCAL3(M_row[j-1] + gap_open_A, \
                                Ix_row[j-1] + gap_open_A, \
                                Iy_row[j-1] + gap_extend_A); \
            Iy_temp = Iy_row[j]; \
            Iy_row[j] = score; \
        } \
        kB = sB[nB-1]; \
        Ix_row[nB] = 0; \
        Iy_row[nB] = 0; \
        SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp, \
                                       Ix_temp, \
                                       Iy_temp, \
                                       (align_score)); \
        M_temp = M_row[nB]; \
        M_row[nB] = score; \
    } \
    M_temp = M_row[0]; \
    Ix_temp = Ix_row[0]; \
    Iy_temp = Iy_row[0]; \
    M_row[0] = -DBL_MAX; \
    Ix_row[0] = 0; \
    Iy_row[0] = -DBL_MAX; \
    kA = sA[nA-1]; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp, \
                                       Ix_temp, \
                                       Iy_temp, \
                                       (align_score)); \
        M_temp = M_row[j]; \
        M_row[j] = score; \
        Ix_temp = Ix_row[j]; \
        Iy_temp = Iy_row[j]; \
        Ix_row[j] = 0; \
        Iy_row[j] = 0; \
    } \
    kB = sB[nB-1]; \
    SELECT_SCORE_GOTOH_LOCAL_ALIGN(M_temp, \
                                   Ix_temp, \
                                   Iy_temp, \
                                   (align_score)); \
    PyMem_Free(M_row); \
    PyMem_Free(Ix_row); \
    PyMem_Free(Iy_row); \
    return PyFloat_FromDouble(maximum); \
exit: \
    if (M_row) PyMem_Free(M_row); \
    if (Ix_row) PyMem_Free(Ix_row); \
    if (Iy_row) PyMem_Free(Iy_row); \
    return PyErr_NoMemory(); \


#define GOTOH_GLOBAL_ALIGN(align_score) \
    int i; \
    int j; \
    int kA; \
    int kB; \
    const double gap_open_A = self->open_internal_insertion_score; \
    const double gap_open_B = self->open_internal_deletion_score; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    double left_gap_open_A; \
    double left_gap_open_B; \
    double left_gap_extend_A; \
    double left_gap_extend_B; \
    double right_gap_open_A; \
    double right_gap_open_B; \
    double right_gap_extend_A; \
    double right_gap_extend_B; \
    const double epsilon = self->epsilon; \
    TraceGapsGotoh** gaps = NULL; \
    Trace** M = NULL; \
    double* M_row = NULL; \
    double* Ix_row = NULL; \
    double* Iy_row = NULL; \
    double score; \
    int trace; \
    double temp; \
    double M_temp; \
    double Ix_temp; \
    double Iy_temp; \
    PathGenerator* paths; \
    switch (strand) { \
        case '+': \
            left_gap_open_A = self->open_left_insertion_score; \
            left_gap_open_B = self->open_left_deletion_score; \
            left_gap_extend_A = self->extend_left_insertion_score; \
            left_gap_extend_B = self->extend_left_deletion_score; \
            right_gap_open_A = self->open_right_insertion_score; \
            right_gap_open_B = self->open_right_deletion_score; \
            right_gap_extend_A = self->extend_right_insertion_score; \
            right_gap_extend_B = self->extend_right_deletion_score; \
            break; \
        case '-': \
            left_gap_open_A = self->open_right_insertion_score; \
            left_gap_open_B = self->open_right_deletion_score; \
            left_gap_extend_A = self->extend_right_insertion_score; \
            left_gap_extend_B = self->extend_right_deletion_score; \
            right_gap_open_A = self->open_left_insertion_score; \
            right_gap_open_B = self->open_left_deletion_score; \
            right_gap_extend_A = self->extend_left_insertion_score; \
            right_gap_extend_B = self->extend_left_deletion_score; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "strand was neither '+' nor '-'"); \
            return NULL; \
    } \
\
    /* Gotoh algorithm with three states */ \
    paths = PathGenerator_create_Gotoh(nA, nB, Global, strand); \
    if (!paths) return NULL; \
    M_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!M_row) goto exit; \
    Ix_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Ix_row) goto exit; \
    Iy_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Iy_row) goto exit; \
    M = paths->M; \
    gaps = paths->gaps.gotoh; \
 \
    /* Gotoh algorithm with three states */ \
    M_row[0] = 0; \
    Ix_row[0] = -DBL_MAX; \
    Iy_row[0] = -DBL_MAX; \
    for (j = 1; j <= nB; j++) { \
        M_row[j] = -DBL_MAX; \
        Ix_row[j] = -DBL_MAX; \
        Iy_row[j] = left_gap_open_A + left_gap_extend_A * (j-1); \
    } \
    for (i = 1; i < nA; i++) { \
        kA = sA[i-1]; \
        M_temp = M_row[0]; \
        Ix_temp = Ix_row[0]; \
        Iy_temp = Iy_row[0]; \
        M_row[0] = -DBL_MAX; \
        Ix_row[0] = left_gap_open_B + left_gap_extend_B * (i-1); \
        Iy_row[0] = -DBL_MAX; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_TRACE_GOTOH_GLOBAL_ALIGN; \
            M_temp = M_row[j]; \
            M_row[j] = score + (align_score); \
            SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix, \
                                          M_temp + gap_open_B, \
                                          Ix_row[j] + gap_extend_B, \
                                          Iy_row[j] + gap_open_B); \
            Ix_temp = Ix_row[j]; \
            Ix_row[j] = score; \
            SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy, \
                                          M_row[j-1] + gap_open_A, \
                                          Ix_row[j-1] + gap_open_A, \
                                          Iy_row[j-1] + gap_extend_A); \
            Iy_temp = Iy_row[j]; \
            Iy_row[j] = score; \
        } \
        kB = sB[nB-1]; \
        SELECT_TRACE_GOTOH_GLOBAL_ALIGN; \
        M_temp = M_row[nB]; \
        M_row[nB] = score + (align_score); \
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix, \
                                      M_temp + right_gap_open_B, \
                                      Ix_row[nB] + right_gap_extend_B, \
                                      Iy_row[nB] + right_gap_open_B); \
        Ix_temp = Ix_row[nB]; \
        Ix_row[nB] = score; \
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy, \
                                      M_row[nB-1] + gap_open_A, \
                                      Ix_row[nB-1] + gap_open_A, \
                                      Iy_row[nB-1] + gap_extend_A); \
        Iy_temp = Iy_row[nB]; \
        Iy_row[nB] = score; \
    } \
    kA = sA[nA-1]; \
    M_temp = M_row[0]; \
    Ix_temp = Ix_row[0]; \
    Iy_temp = Iy_row[0]; \
    M_row[0] = -DBL_MAX; \
    Ix_row[0] = left_gap_open_B + left_gap_extend_B * (nA-1); \
    Iy_row[0] = -DBL_MAX; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_TRACE_GOTOH_GLOBAL_ALIGN; \
        M_temp = M_row[j]; \
        M_row[j] = score + (align_score); \
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix, \
                                      M_temp + gap_open_B, \
                                      Ix_row[j] + gap_extend_B, \
                                      Iy_row[j] + gap_open_B); \
        Ix_temp = Ix_row[j]; \
        Ix_row[j] = score; \
        SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy, \
                                      M_row[j-1] + right_gap_open_A, \
                                      Ix_row[j-1] + right_gap_open_A, \
                                      Iy_row[j-1] + right_gap_extend_A); \
        Iy_temp = Iy_row[j]; \
        Iy_row[j] = score; \
    } \
    kB = sB[nB-1]; \
    SELECT_TRACE_GOTOH_GLOBAL_ALIGN; \
    M_temp = M_row[j]; \
    M_row[j] = score + (align_score); \
    SELECT_TRACE_GOTOH_GLOBAL_GAP(Ix, \
                                  M_temp + right_gap_open_B, \
                                  Ix_row[j] + right_gap_extend_B, \
                                  Iy_row[j] + right_gap_open_B); \
    Ix_row[nB] = score; \
    SELECT_TRACE_GOTOH_GLOBAL_GAP(Iy, \
                                  M_row[j-1] + right_gap_open_A, \
                                  Ix_row[j-1] + right_gap_open_A, \
                                  Iy_row[j-1] + right_gap_extend_A); \
    Iy_row[nB] = score; \
    M[nA][nB].path = 0; \
 \
    /* traceback */ \
    SELECT_SCORE_GLOBAL(M_row[nB], Ix_row[nB], Iy_row[nB]); \
    if (M_row[nB] < score - epsilon) M[nA][nB].trace = 0; \
    if (Ix_row[nB] < score - epsilon) gaps[nA][nB].Ix = 0; \
    if (Iy_row[nB] < score - epsilon) gaps[nA][nB].Iy = 0; \
    PyMem_Free(M_row); \
    PyMem_Free(Ix_row); \
    PyMem_Free(Iy_row); \
    return Py_BuildValue("fN", score, paths); \
exit: \
    Py_DECREF(paths); \
    if (M_row) PyMem_Free(M_row); \
    if (Ix_row) PyMem_Free(Ix_row); \
    if (Iy_row) PyMem_Free(Iy_row); \
    return PyErr_NoMemory(); \


#define GOTOH_LOCAL_ALIGN(align_score) \
    int i; \
    int j; \
    int im = nA; \
    int jm = nB; \
    int kA; \
    int kB; \
    const double gap_open_A = self->open_internal_insertion_score; \
    const double gap_open_B = self->open_internal_deletion_score; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    const double epsilon = self->epsilon; \
    Trace** M = NULL; \
    TraceGapsGotoh** gaps = NULL; \
    double* M_row = NULL; \
    double* Ix_row = NULL; \
    double* Iy_row = 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, strand); \
    if (!paths) return NULL; \
    M = paths->M; \
    gaps = paths->gaps.gotoh; \
    M_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!M_row) goto exit; \
    Ix_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Ix_row) goto exit; \
    Iy_row = PyMem_Malloc((nB+1)*sizeof(double)); \
    if (!Iy_row) goto exit; \
    M_row[0] = 0; \
    Ix_row[0] = -DBL_MAX; \
    Iy_row[0] = -DBL_MAX; \
    for (j = 1; j <= nB; j++) { \
        M_row[j] = 0; \
        Ix_row[j] = -DBL_MAX; \
        Iy_row[j] = -DBL_MAX; \
    } \
    for (i = 1; i < nA; i++) { \
        M_temp = M_row[0]; \
        Ix_temp = Ix_row[0]; \
        Iy_temp = Iy_row[0]; \
        M_row[0] = 0; \
        Ix_row[0] = -DBL_MAX; \
        Iy_row[0] = -DBL_MAX; \
        kA = sA[i-1]; \
        for (j = 1; j < nB; j++) { \
            kB = sB[j-1]; \
            SELECT_TRACE_GOTOH_LOCAL_ALIGN(align_score) \
            M_temp = M_row[j]; \
            M_row[j] = score; \
            SELECT_TRACE_GOTOH_LOCAL_GAP(Ix, \
                                     M_temp + gap_open_B, \
                                     Ix_row[j] + gap_extend_B, \
                                     Iy_row[j] + gap_open_B); \
            Ix_temp = Ix_row[j]; \
            Ix_row[j] = score; \
            SELECT_TRACE_GOTOH_LOCAL_GAP(Iy, \
                                     M_row[j-1] + gap_open_A, \
                                     Ix_row[j-1] + gap_open_A, \
                                     Iy_row[j-1] + gap_extend_A); \
            Iy_temp = Iy_row[j]; \
            Iy_row[j] = score; \
        } \
        kB = sB[nB-1]; \
        SELECT_TRACE_GOTOH_LOCAL_ALIGN(align_score) \
        M_temp = M_row[j]; \
        M_row[j] = score; \
        Ix_temp = Ix_row[nB]; \
        Ix_row[nB] = 0; \
        gaps[i][nB].Ix = 0; \
        Iy_temp = Iy_row[nB]; \
        Iy_row[nB] = 0; \
        gaps[i][nB].Iy = 0; \
    } \
    M_temp = M_row[0]; \
    M_row[0] = 0; \
    M[nA][0].trace = 0; \
    Ix_temp = Ix_row[0]; \
    Ix_row[0] = -DBL_MAX; \
    gaps[nA][0].Ix = 0; \
    gaps[nA][0].Iy = 0; \
    Iy_temp = Iy_row[0]; \
    Iy_row[0] = -DBL_MAX; \
    kA = sA[nA-1]; \
    for (j = 1; j < nB; j++) { \
        kB = sB[j-1]; \
        SELECT_TRACE_GOTOH_LOCAL_ALIGN(align_score) \
        M_temp = M_row[j]; \
        M_row[j] = score; \
        Ix_temp = Ix_row[j]; \
        Ix_row[j] = 0; \
        gaps[nA][j].Ix = 0; \
        Iy_temp = Iy_row[j]; \
        Iy_row[j] = 0; \
        gaps[nA][j].Iy = 0; \
    } \
    kB = sB[nB-1]; \
    SELECT_TRACE_GOTOH_LOCAL_ALIGN(align_score) \
    gaps[nA][nB].Ix = 0; \
    gaps[nA][nB].Iy = 0; \
\
    PyMem_Free(M_row); \
    PyMem_Free(Ix_row); \
    PyMem_Free(Iy_row); \
\
    /* As we don't allow zero-score extensions to alignments, \
     * we need to remove all traces towards an ENDPOINT. \
     * In addition, some points then won't have any path to a STARTPOINT. \
     * Here, use path as a temporary variable to indicate if the point \
     * is reachable from a STARTPOINT. If it is unreachable, remove all \
     * traces from it, and don't allow it to be an ENDPOINT. It may still \
     * be a valid STARTPOINT. */ \
    for (j = 0; j <= nB; j++) M[0][j].path = M_MATRIX; \
    for (i = 1; i <= nA; i++) { \
        M[i][0].path = M_MATRIX; \
        for (j = 1; j <= nB; j++) { \
            /* Remove traces to unreachable points. */ \
            trace = M[i][j].trace; \
            if (!(M[i-1][j-1].path & M_MATRIX)) trace &= ~M_MATRIX; \
            if (!(M[i-1][j-1].path & Ix_MATRIX)) trace &= ~Ix_MATRIX; \
            if (!(M[i-1][j-1].path & Iy_MATRIX)) trace &= ~Iy_MATRIX; \
            if (trace & (STARTPOINT | M_MATRIX | Ix_MATRIX | Iy_MATRIX)) { \
                /* The point is reachable. */ \
                if (trace & ENDPOINT) M[i][j].path = 0; /* no extensions after ENDPOINT */ \
                else M[i][j].path |= M_MATRIX; \
            } \
            else { \
                /* The point is not reachable. Then it is not a STARTPOINT, \
                 * all traces from it can be removed, and it cannot act as \
                 * an ENDPOINT. */ \
                M[i][j].path &= ~M_MATRIX; \
                trace = 0; \
            } \
            M[i][j].trace = trace; \
            trace = gaps[i][j].Ix; \
            if (!(M[i-1][j].path & M_MATRIX)) trace &= ~M_MATRIX; \
            if (!(M[i-1][j].path & Ix_MATRIX)) trace &= ~Ix_MATRIX; \
            if (!(M[i-1][j].path & Iy_MATRIX)) trace &= ~Iy_MATRIX; \
            if (trace & (M_MATRIX | Ix_MATRIX | Iy_MATRIX)) { \
                /* The point is reachable. */ \
                M[i][j].path |= Ix_MATRIX; \
            } \
            else { \
                /* The point is not reachable. Then \
                 * all traces from it can be removed. */ \
                M[i][j].path &= ~Ix_MATRIX; \
                trace = 0; \
            } \
            gaps[i][j].Ix = trace; \
            trace = gaps[i][j].Iy; \
            if (!(M[i][j-1].path & M_MATRIX)) trace &= ~M_MATRIX; \
            if (!(M[i][j-1].path & Ix_MATRIX)) trace &= ~Ix_MATRIX; \
            if (!(M[i][j-1].path & Iy_MATRIX)) trace &= ~Iy_MATRIX; \
            if (trace & (M_MATRIX | Ix_MATRIX | Iy_MATRIX)) { \
                /* The point is reachable. */ \
                M[i][j].path |= Iy_MATRIX; \
            } \
            else { \
                /* The point is not reachable. Then \
                 * all traces from it can be removed. */ \
                M[i][j].path &= ~Iy_MATRIX; \
                trace = 0; \
            } \
            gaps[i][j].Iy = trace; \
        } \
    } \
\
    /* traceback */ \
    if (maximum == 0) M[0][0].path = DONE; \
    else M[0][0].path = 0; \
    return Py_BuildValue("fN", maximum, paths); \
\
exit: \
    Py_DECREF(paths); \
    if (M_row) PyMem_Free(M_row); \
    if (Ix_row) PyMem_Free(Ix_row); \
    if (Iy_row) PyMem_Free(Iy_row); \
    return PyErr_NoMemory(); \


#define WATERMANSMITHBEYER_ENTER_SCORE \
    int i; \
    int j = 0; \
    int k; \
    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; \
    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; \
    } \


#define WATERMANSMITHBEYER_GLOBAL_SCORE(align_score, query_gap_start) \
    /* 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; \
        Iy[i][0] = -DBL_MAX; \
        ok = _call_deletion_score_function(self, query_gap_start, i, nB, &score); \
        if (!ok) goto exit; \
        Ix[i][0] = score; \
    } \
    for (j = 1; j <= nB; j++) { \
        M[0][j] = -DBL_MAX; \
        Ix[0][j] = -DBL_MAX; \
        ok = _call_insertion_score_function(self, 0, j, nA, &score); \
        if (!ok) goto exit; \
        Iy[0][j] = score; \
    } \
    for (i = 1; i <= nA; i++) { \
        kA = sA[i-1]; \
        for (j = 1; j <= nB; j++) { \
            kB = 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 + (align_score); \
            score = -DBL_MAX; \
            for (k = 1; k <= i; k++) { \
                ok = _call_deletion_score_function(self, query_gap_start, k, nB, &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_insertion_score_function(self, i, k, nA, &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]); \
\
    result = PyFloat_FromDouble(score); \


#define WATERMANSMITHBEYER_LOCAL_SCORE(align_score, query_gap_start) \
    /* 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 = sA[i-1]; \
        for (j = 1; j <= nB; j++) { \
            kB = sB[j-1]; \
            SELECT_SCORE_GOTOH_LOCAL_ALIGN(M[i-1][j-1], \
                                           Ix[i-1][j-1], \
                                           Iy[i-1][j-1], \
                                           (align_score)); \
            M[i][j] = score; \
            if (i == nA || j == nB) { \
                Ix[i][j] = 0; \
                Iy[i][j] = 0; \
                continue; \
            } \
            score = 0.0; \
            for (k = 1; k <= i; k++) { \
                ok = _call_deletion_score_function(self, query_gap_start, k, nB, &gapscore); \
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i-k][j], Iy[i-k][j]); \
                if (!ok) goto exit; \
            } \
            if (score > maximum) maximum = score; \
            Ix[i][j] = score; \
            score = 0.0; \
            for (k = 1; k <= j; k++) { \
                ok = _call_insertion_score_function(self, i, k, nA, &gapscore); \
                if (!ok) goto exit; \
                SELECT_SCORE_WATERMAN_SMITH_BEYER(M[i][j-k], Ix[i][j-k]); \
            } \
            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); \


#define WATERMANSMITHBEYER_EXIT_SCORE \
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; \


#define WATERMANSMITHBEYER_ENTER_ALIGN(mode) \
    int i; \
    int j = 0; \
    int gap; \
    int kA; \
    int kB; \
    const double epsilon = self->epsilon; \
    Trace** M; \
    TraceGapsWatermanSmithBeyer** gaps; \
    double** M_row = NULL; \
    double** Ix_row = NULL; \
    double** Iy_row = 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, mode, strand); \
    if (!paths) return NULL; \
    M = paths->M; \
    gaps = paths->gaps.waterman_smith_beyer; \
    M_row = PyMem_Malloc((nA+1)*sizeof(double*)); \
    if (!M_row) goto exit; \
    Ix_row = PyMem_Malloc((nA+1)*sizeof(double*)); \
    if (!Ix_row) goto exit; \
    Iy_row = PyMem_Malloc((nA+1)*sizeof(double*)); \
    if (!Iy_row) goto exit; \
    for (i = 0; i <= nA; i++) { \
        M_row[i] = PyMem_Malloc((nB+1)*sizeof(double)); \
        if (!M_row[i]) goto exit; \
        Ix_row[i] = PyMem_Malloc((nB+1)*sizeof(double)); \
        if (!Ix_row[i]) goto exit; \
        Iy_row[i] = PyMem_Malloc((nB+1)*sizeof(double)); \
        if (!Iy_row[i]) goto exit; \
    } \


#define WATERMANSMITHBEYER_GLOBAL_ALIGN(align_score, query_gap_start) \
    M_row[0][0] = 0; \
    Ix_row[0][0] = -DBL_MAX; \
    Iy_row[0][0] = -DBL_MAX; \
    for (i = 1; i <= nA; i++) { \
        M_row[i][0] = -DBL_MAX; \
        Iy_row[i][0] = -DBL_MAX; \
        ok = _call_deletion_score_function(self, query_gap_start, i, nB, &score); \
        if (!ok) goto exit; \
        Ix_row[i][0] = score; \
    } \
    for (j = 1; j <= nB; j++) { \
        M_row[0][j] = -DBL_MAX; \
        Ix_row[0][j] = -DBL_MAX; \
        ok = _call_insertion_score_function(self, 0, j, nA, &score); \
        if (!ok) goto exit; \
        Iy_row[0][j] = score; \
    } \
    for (i = 1; i <= nA; i++) { \
        kA = sA[i-1]; \
        for (j = 1; j <= nB; j++) { \
            kB = sB[j-1]; \
            SELECT_TRACE_WATERMAN_SMITH_BEYER_GLOBAL_ALIGN((align_score)); \
            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_deletion_score_function(self, query_gap_start, gap, nB, &gapscore); \
                if (!ok) goto exit; \
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_row[i-gap][j], \
                                                      Iy_row[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_row[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_insertion_score_function(self, i, gap, nA, &gapscore); \
                if (!ok) goto exit; \
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_row[i][j-gap], \
                                                      Ix_row[i][j-gap]); \
            } \
            Iy_row[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_row[nA][nB], Ix_row[nA][nB], Iy_row[nA][nB]); \
    M[nA][nB].path = 0; \
    if (M_row[nA][nB] < score - epsilon) M[nA][nB].trace = 0; \
    if (Ix_row[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_row[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_row[i]); \
        PyMem_Free(Ix_row[i]); \
        PyMem_Free(Iy_row[i]); \
    } \
    PyMem_Free(M_row); \
    PyMem_Free(Ix_row); \
    PyMem_Free(Iy_row); \
    return Py_BuildValue("fN", score, paths); \


#define WATERMANSMITHBEYER_LOCAL_ALIGN(align_score, query_gap_start) \
    M_row[0][0] = 0; \
    Ix_row[0][0] = -DBL_MAX; \
    Iy_row[0][0] = -DBL_MAX; \
    for (i = 1; i <= nA; i++) { \
        M_row[i][0] = 0; \
        Ix_row[i][0] = -DBL_MAX; \
        Iy_row[i][0] = -DBL_MAX; \
    } \
    for (i = 1; i <= nB; i++) { \
        M_row[0][i] = 0; \
        Ix_row[0][i] = -DBL_MAX; \
        Iy_row[0][i] = -DBL_MAX; \
    } \
    for (i = 1; i <= nA; i++) { \
        kA = sA[i-1]; \
        for (j = 1; j <= nB; j++) { \
            kB = sB[j-1]; \
            nm = 0; \
            ng = 0; \
            SELECT_TRACE_WATERMAN_SMITH_BEYER_ALIGN( \
                                           M_row[i-1][j-1], \
                                           Ix_row[i-1][j-1], \
                                           Iy_row[i-1][j-1], \
                                           (align_score)); \
            M[i][j].path = 0; \
            if (i == nA || j == nB) { \
                Ix_row[i][j] = score; \
                gaps[i][j].MIx = NULL; \
                gaps[i][j].IyIx = NULL; \
                gaps[i][j].MIy = NULL; \
                gaps[i][j].IxIy = NULL; \
                Iy_row[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_deletion_score_function(self, query_gap_start, gap, nB, &gapscore); \
                if (!ok) goto exit; \
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_row[i-gap][j], \
                                                      Iy_row[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_row[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_insertion_score_function(self, i, gap, nA, &gapscore); \
                if (!ok) goto exit; \
                SELECT_TRACE_WATERMAN_SMITH_BEYER_GAP(M_row[i][j-gap], \
                                                      Ix_row[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_row[i][j] = score; \
            M[i][j].path = 0; \
        } \
    } \
    for (i = 0; i <= nA; i++) PyMem_Free(M_row[i]); \
    PyMem_Free(M_row); \
    for (i = 0; i <= nA; i++) PyMem_Free(Ix_row[i]); \
    PyMem_Free(Ix_row); \
    for (i = 0; i <= nA; i++) PyMem_Free(Iy_row[i]); \
    PyMem_Free(Iy_row); \
\
    /* As we don't allow zero-score extensions to alignments, \
     * we need to remove all traces towards an ENDPOINT. \
     * In addition, some points then won't have any path to a STARTPOINT. \
     * Here, use path as a temporary variable to indicate if the point \
     * is reachable from a STARTPOINT. If it is unreachable, remove all \
     * traces from it, and don't allow it to be an ENDPOINT. It may still \
     * be a valid STARTPOINT. */ \
    for (j = 0; j <= nB; j++) M[0][j].path = M_MATRIX; \
    for (i = 1; i <= nA; i++) { \
        M[i][0].path = M_MATRIX; \
        for (j = 1; j <= nB; j++) { \
            /* Remove traces to unreachable points. */ \
            trace = M[i][j].trace; \
            if (!(M[i-1][j-1].path & M_MATRIX)) trace &= ~M_MATRIX; \
            if (!(M[i-1][j-1].path & Ix_MATRIX)) trace &= ~Ix_MATRIX; \
            if (!(M[i-1][j-1].path & Iy_MATRIX)) trace &= ~Iy_MATRIX; \
            if (trace & (STARTPOINT | M_MATRIX | Ix_MATRIX | Iy_MATRIX)) { \
                /* The point is reachable. */ \
                if (trace & ENDPOINT) M[i][j].path = 0; /* no extensions after ENDPOINT */ \
                else M[i][j].path |= M_MATRIX; \
            } \
            else { \
                /* The point is not reachable. Then it is not a STARTPOINT, \
                 * all traces from it can be removed, and it cannot act as \
                 * an ENDPOINT. */ \
                M[i][j].path &= ~M_MATRIX; \
                trace = 0; \
            } \
            M[i][j].trace = trace; \
            if (i == nA || j == nB) continue; \
            gapM = gaps[i][j].MIx; \
            gapXY = gaps[i][j].IyIx; \
            nm = 0; \
            ng = 0; \
            for (im = 0; (gap = gapM[im]); im++) \
                if (M[i-gap][j].path & M_MATRIX) gapM[nm++] = gap; \
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int)); \
            if (!gapM) goto exit; \
            gapM[nm] = 0; \
            gaps[i][j].MIx = gapM; \
            for (im = 0; (gap = gapXY[im]); im++) \
                if (M[i-gap][j].path & Iy_MATRIX) gapXY[ng++] = gap; \
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int)); \
            if (!gapXY) goto exit; \
            gapXY[ng] = 0; \
            gaps[i][j].IyIx = gapXY; \
            if (nm==0 && ng==0) M[i][j].path &= ~Ix_MATRIX; /* not reachable */ \
            else M[i][j].path |= Ix_MATRIX; /* reachable */ \
            gapM = gaps[i][j].MIy; \
            gapXY = gaps[i][j].IxIy; \
            nm = 0; \
            ng = 0; \
            for (im = 0; (gap = gapM[im]); im++) \
                if (M[i][j-gap].path & M_MATRIX) gapM[nm++] = gap; \
            gapM = PyMem_Realloc(gapM, (nm+1)*sizeof(int)); \
            if (!gapM) goto exit; \
            gapM[nm] = 0; \
            gaps[i][j].MIy = gapM; \
            for (im = 0; (gap = gapXY[im]); im++) \
                if (M[i][j-gap].path & Ix_MATRIX) gapXY[ng++] = gap; \
            gapXY = PyMem_Realloc(gapXY, (ng+1)*sizeof(int)); \
            if (!gapXY) goto exit; \
            gapXY[ng] = 0; \
            gaps[i][j].IxIy = gapXY; \
            if (nm==0 && ng==0) M[i][j].path &= ~Iy_MATRIX; /* not reachable */ \
            else M[i][j].path |= Iy_MATRIX; /* reachable */ \
        } \
    } \
    /* traceback */ \
    if (maximum == 0) M[0][0].path = DONE; \
    else M[0][0].path = 0; \
    return Py_BuildValue("fN", maximum, paths); \


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


#define FOGSAA_ENTER \
    int i, j; \
    double t, t2; /* temporary variables */ \
    int kA, kB; \
    int curpA = 0, curpB = 0; /* optimal and current pointers */ \
    int pathend = 1, child_types[3]; \
    double lower_bound, child_lbounds[3], child_ubounds[3]; \
    /* pathend denotes if the current path is active, expanded is the number of \
     * expanded nodes, lower_bound contains the global lower_bound, a and b \
     * contain the lower bounds for the current cell. ch contains the types of \
     * the potential children */ \
    int type_total = 1; \
    /* The initial values for new_type, npA, npB, new_score, new_lower, \
    new_upper, next_lower, and next_upper don't mean anything; they're never \
    used and are only initialized to stop compiler warnings */ \
    int new_type = 0, npA = 0, npB = 0; \
    double new_score = 0, new_lower = 0, new_upper = 0, next_lower = 0, \
        next_upper = 0; \
    const double gap_open_A = self->open_internal_insertion_score; \
    const double gap_open_B = self->open_internal_deletion_score; \
    const double gap_extend_A = self->extend_internal_insertion_score; \
    const double gap_extend_B = self->extend_internal_deletion_score; \
    struct fogsaa_cell* matrix = NULL; \
    struct fogsaa_queue queue; \
    double left_gap_open_A; \
    double left_gap_open_B; \
    double left_gap_extend_A; \
    double left_gap_extend_B; \
    double right_gap_open_A; \
    double right_gap_open_B; \
    double right_gap_extend_A; \
    double right_gap_extend_B; \
    switch (strand) { \
        case '+': \
            left_gap_open_A = self->open_left_insertion_score; \
            left_gap_open_B = self->open_left_deletion_score; \
            left_gap_extend_A = self->extend_left_insertion_score; \
            left_gap_extend_B = self->extend_left_deletion_score; \
            right_gap_open_A = self->open_right_insertion_score; \
            right_gap_open_B = self->open_right_deletion_score; \
            right_gap_extend_A = self->extend_right_insertion_score; \
            right_gap_extend_B = self->extend_right_deletion_score; \
            break; \
        case '-': \
            left_gap_open_A = self->open_right_insertion_score; \
            left_gap_open_B = self->open_right_deletion_score; \
            left_gap_extend_A = self->extend_right_insertion_score; \
            left_gap_extend_B = self->extend_right_deletion_score; \
            right_gap_open_A = self->open_left_insertion_score; \
            right_gap_open_B = self->open_left_deletion_score; \
            right_gap_extend_A = self->extend_left_insertion_score; \
            right_gap_extend_B = self->extend_left_deletion_score; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "strand was neither '+' nor '-'"); \
            return NULL; \
    } \

#define FOGSAA_DO(align_score) \
    /* allocate and initialize matrix */ \
    matrix = PyMem_Calloc((nA+1) * (nB+1), sizeof(struct fogsaa_cell)); \
    if (!matrix) \
        return PyErr_NoMemory(); \
    MATRIX(0, 0).present_score = 0; \
    MATRIX(0, 0).type = STARTPOINT; \
    FOGSAA_CALCULATE_SCORE(MATRIX(0, 0).present_score, STARTPOINT, MATRIX(0, 0).lower, MATRIX(0, 0).upper, 0, 0); \
    MATRIX(0, 0).is_left_gap = 1; \
    lower_bound = MATRIX(0, 0).lower; \
    \
    /* initialize queue */ \
    queue.array = NULL; \
    queue.size = 0; \
    queue.capacity = 0; \
    /* main loop */ \
    do { \
        pathend = 1; \
        while (curpA < nA || curpB < nB) { \
            struct fogsaa_cell *curr = &(MATRIX(curpA, curpB)); \
            if (type_total == DIAGONAL || type_total == HORIZONTAL || type_total == VERTICAL) { \
                /* current is a 1st child */ \
                if (curpA <= nA - 1 && curpB <= nB - 1) { \
                    /* neither sequence is at the end, so we can advance in both sequences */ \
                    kA = sA[curpA]; \
                    kB = sB[curpB]; \
                    double p = align_score; \
                    /* score the match/mismatch */ \
                    FOGSAA_CALCULATE_SCORE(curr->present_score + p, DIAGONAL, child_lbounds[0], child_ubounds[0], curpA + 1, curpB + 1); \
                    /* score the gaps */ \
                    if (curr->type == DIAGONAL || curr->type == STARTPOINT) { \
                        if (!curr->is_left_gap) { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } else { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } \
                    } else if (curr->type == HORIZONTAL) { \
                        /* gap is already opened in the first chain */ \
                        if (!curr->is_left_gap) { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } else { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } \
                    } else { \
                        /* gap is already opened in the 2nd chain */ \
                        if (!curr->is_left_gap) { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } else { \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_A, HORIZONTAL, child_lbounds[1], child_ubounds[1], curpA, curpB + 1) \
                            FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_B, VERTICAL, child_lbounds[2], child_ubounds[2], curpA + 1, curpB) \
                        } \
                    } \
                    \
                    /* sort and select the best new child as the new type */ \
                    child_types[0] = DIAGONAL; \
                    child_types[1] = HORIZONTAL; \
                    child_types[2] = VERTICAL; \
                    FOGSAA_SORT() \
                    new_type = child_types[0]; \
                    if (new_type == DIAGONAL) { \
                        npA = curpA + 1; \
                        npB = curpB + 1; \
                        new_score = curr->present_score + p; \
                    } else if (new_type == HORIZONTAL) { \
                        npA = curpA; \
                        npB = curpB + 1; \
                        if (curr->is_left_gap) { \
                            new_score = curr->present_score + (curr->type == HORIZONTAL ? left_gap_extend_A : left_gap_open_A); \
                        } else { \
                            new_score = curr->present_score + (curr->type == HORIZONTAL ? gap_extend_A : gap_open_A); \
                        } \
                    } else { \
                        /* new_type is VERTICAL */ \
                        npA = curpA + 1; \
                        npB = curpB; \
                        if (curr->is_left_gap) { \
                            new_score = curr->present_score + (curr->type == VERTICAL ? left_gap_extend_B : left_gap_open_B); \
                        } else { \
                            new_score = curr->present_score + (curr->type == VERTICAL ? gap_extend_B : gap_open_B); \
                        } \
                    } \
                    if (child_ubounds[1] >= MATRIX(0, 0).lower) { \
                        /* insert 2nd best new child to the queue */ \
                        if (!fogsaa_queue_insert(&queue, curpA, curpB, new_type + child_types[1], child_types[1], child_lbounds[1], child_ubounds[1])) \
                            return PyErr_NoMemory(); \
                    } \
                } else if (curpA <= nA - 1) { \
                    /* we're at the end of B, so must put a gap in B */ \
                    new_type = VERTICAL; \
                    npA = curpA + 1; \
                    npB = curpB; \
                    new_score = curr->present_score + (curr->type == VERTICAL ? right_gap_extend_B : right_gap_open_B); \
                } else { \
                    /* we're at the end of A, so must put a gap in A */ \
                    new_type = HORIZONTAL; \
                    npA = curpA; \
                    npB = curpB + 1; \
                    new_score = curr->present_score + (curr->type == HORIZONTAL ? right_gap_extend_A : right_gap_open_A); \
                } \
            } else if (type_total == DIAGONAL + HORIZONTAL || \
                    type_total == DIAGONAL + VERTICAL || \
                    type_total == HORIZONTAL + VERTICAL) { \
                /* current is a 2nd child (sum of two types) */ \
                if (new_type == DIAGONAL) { \
                    npA = curpA + 1; \
                    npB = curpB + 1; \
                    new_score = curr->present_score + (sA[curpA] == sB[curpB] ? match : mismatch); \
                    /* find what the 3rd child was (will later be added to the queue) */ \
                    /* NOTE: DIAGONAL + HORIZONTAL + VERTICAL = 7 */ \
                    if (7 - type_total == HORIZONTAL) { \
                        if (curr->type != HORIZONTAL) { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } else { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } \
                    } else { \
                        /* 3rd child was VERTICAL */ \
                        if (curr->type != VERTICAL) { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } else { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } \
                    } \
                } else if (new_type == HORIZONTAL) { \
                    npA = curpA; \
                    npB = curpB + 1; \
                    new_score = curr->present_score + (curr->type == HORIZONTAL ? gap_extend_A : gap_open_A); \
                    /* again, find what 3rd child was */ \
                    if (7 - type_total == DIAGONAL) { \
                        kA = sA[curpA]; \
                        kB = sB[curpB]; \
                        FOGSAA_CALCULATE_SCORE(curr->present_score + (align_score), DIAGONAL, next_lower, next_upper, curpA + 1, curpB + 1); \
                    } else { \
                        /* 3rd child was VERTICAL */ \
                        if (curr->type != VERTICAL) { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } else { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_B, VERTICAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } \
                    } \
                } else { \
                    /* new_type is VERTICAL */ \
                    npA = curpA + 1; \
                    npB = curpB; \
                    new_score = curr->present_score + (curr->type == VERTICAL ? gap_extend_B : gap_open_B); \
                    /* again, find what 3rd child was */ \
                    if (7 - type_total == DIAGONAL) { \
                        kA = sA[curpA]; \
                        kB = sB[curpB]; \
                        FOGSAA_CALCULATE_SCORE(curr->present_score + (align_score), DIAGONAL, next_lower, next_upper, curpA + 1, curpB + 1); \
                    } else { \
                        /* 3rd child was HORIZONTAL */ \
                        if (curr->type != HORIZONTAL) { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_open_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_open_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } else { \
                            if (curr->is_left_gap) { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + left_gap_extend_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } else { \
                                FOGSAA_CALCULATE_SCORE(curr->present_score + gap_extend_A, HORIZONTAL, next_lower, next_upper, curpA, curpB + 1) \
                            } \
                        } \
                    } \
                } \
                if (next_upper >= MATRIX(0, 0).lower) { \
                    if (!fogsaa_queue_insert(&queue, curpA, curpB, 7, 7 - type_total, next_lower, next_upper)) \
                        return PyErr_NoMemory(); \
                } \
            } else if (type_total == DIAGONAL + HORIZONTAL + VERTICAL) { \
                /* current is a 3rd child */ \
                if (new_type == DIAGONAL) { \
                    kA = sA[curpA]; \
                    kB = sB[curpB]; \
                    npA = curpA + 1; \
                    npB = curpB + 1; \
                    new_score = curr->present_score + (align_score); \
                } else if (new_type == HORIZONTAL) { \
                    npA = curpA; \
                    npB = curpB + 1; \
                    if (curr->type != HORIZONTAL) { \
                        new_score = curr->present_score + (curr->is_left_gap ? left_gap_open_A : gap_open_A); \
                    } else { \
                        new_score = curr->present_score + (curr->is_left_gap ? left_gap_extend_A : gap_extend_A); \
                    } \
                } else { \
                    /* new_type is VERTICAL */ \
                    npA = curpA + 1; \
                    npB = curpB; \
                    if (curr->type != VERTICAL) { \
                        new_score = curr->present_score + (curr->is_left_gap ? left_gap_open_B : gap_open_B); \
                    } else { \
                        new_score = curr->present_score + (curr->is_left_gap ? left_gap_extend_B : gap_extend_B); \
                    } \
                } \
                /* no more nodes to insert into the queue */ \
            } \
            \
            /* write the new node to the matrix, but skip if there's already a better path there */ \
            if (MATRIX(npA, npB).filled == 1 && MATRIX(npA, npB).type <= 4 && \
                    MATRIX(npA, npB).present_score >= new_score) { \
                pathend = 0; \
                break; \
            } else { \
                FOGSAA_CALCULATE_SCORE(new_score, new_type, new_lower, new_upper, npA, npB) \
                MATRIX(npA, npB).present_score = new_score; \
                MATRIX(npA, npB).lower = new_lower; \
                MATRIX(npA, npB).upper = new_upper; \
                MATRIX(npA, npB).type = new_type; \
                MATRIX(npA, npB).filled = 1; \
                if (new_type == HORIZONTAL || new_type == VERTICAL) { \
                    MATRIX(npA, npB).is_left_gap = curr->is_left_gap; \
                } else { \
                    MATRIX(npA, npB).is_left_gap = 0; \
                } \
            } \
            \
            /* make the child the new current node */ \
            curpA = npA; \
            curpB = npB; \
            type_total = 1; \
            \
            if (MATRIX(npA, npB).upper < lower_bound && \
                    lower_bound - MATRIX(npA, npB).upper > self->epsilon) { \
                pathend = 0; \
                break; \
            } \
        } \
        \
        if (MATRIX(curpA, curpB).present_score > lower_bound && \
                MATRIX(curpA, curpB).present_score - lower_bound > self->epsilon && \
                pathend == 1) { \
            /* if this is the best score and we've fully expanded the branch, set it as the new lower bound */ \
            lower_bound = MATRIX(curpA, curpB).present_score; \
        } \
        \
        /* If possible, pop the next best from the queue */ \
        if (queue.size > 0) { \
            struct fogsaa_queue_node root = fogsaa_queue_pop(&queue); \
            curpA = root.pA; \
            curpB = root.pB; \
            type_total = root.type_upto_next; \
            new_lower = root.next_lower; \
            new_upper = root.next_upper; \
            new_type = root.next_type; \
        } else { \
            break; \
        } \
    } while (lower_bound < new_upper && new_upper - lower_bound > self->epsilon); \
    \
    /* cleanup and return */ \
    PyMem_Free(queue.array);


#define FOGSAA_EXIT_SCORE \
    if (lower_bound < new_upper && new_upper - lower_bound > self->epsilon) { \
        PyErr_Format(PyExc_RuntimeError, "Algorithm ended incomplete. Report this as a bug."); \
        return NULL; \
    } \
    t = MATRIX(nA, nB).present_score; \
    PyMem_Free(matrix); \
    return PyFloat_FromDouble((double)t);

#define FOGSAA_EXIT_ALIGN \
    if (lower_bound < new_upper && new_upper - lower_bound > self->epsilon) { \
        PyErr_SetString(PyExc_RuntimeError, "Algorithm ended incomplete. Report this as a bug."); \
        return NULL; \
    } \
    paths = PathGenerator_create_FOGSAA(nA, nB, strand); \
    M = paths->M; \
    if (!paths) return NULL; \
    \
    /* copy only the cells of the optimal path to trace and path */ \
    i = nA; \
    j = nB; \
    while (1) { \
        switch (MATRIX(i, j).type) { \
        case 0: \
        case STARTPOINT: \
            M[i][j].trace = 0; \
            goto end_loop; \
        case DIAGONAL: \
            M[i][j].trace = DIAGONAL; \
            M[--i][--j].path = DIAGONAL; \
            break; \
        case HORIZONTAL: \
            M[i][j].trace = HORIZONTAL; \
            M[i][--j].path = HORIZONTAL; \
            break; \
        case VERTICAL: \
            M[i][j].trace = VERTICAL; \
            M[--i][j].path = VERTICAL; \
            break; \
        default: \
            PyErr_SetString(PyExc_RuntimeError, "Unexpected FOGSAA cell type. Report this as a bug."); \
            return NULL; \
        } \
    } \
end_loop: \
    M[nA][nB].path = 0; \
    t = MATRIX(nA, nB).present_score; \
    PyMem_Free(matrix); \
    return Py_BuildValue("fN", (double)t, paths);


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

static PathGenerator*
PathGenerator_create_NWSW(int nA, int nB, Mode mode, unsigned char strand)
{
    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;
    paths->strand = strand;

    M = PyMem_Malloc((nA+1)*sizeof(Trace*));
    paths->M = M;
    if (!M) goto exit;
    switch (mode) {
        case Global: trace = VERTICAL; break;
        case Local: trace = STARTPOINT; break;
        default:
            ERR_UNEXPECTED_MODE
            return NULL;
    }
    for (i = 0; i <= nA; i++) {
        M[i] = PyMem_Malloc((nB+1)*sizeof(Trace));
        if (!M[i]) goto exit;
        M[i][0].trace = trace;
    }
    if (mode == Global) {
        M[0][0].trace = 0;
        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(int nA, int nB, Mode mode, unsigned char strand)
{
    int i;
    unsigned char trace;
    Trace** M;
    TraceGapsGotoh** gaps;
    PathGenerator* paths;

    switch (mode) {
        case Global: trace = 0; break;
        case Local: trace = STARTPOINT; break;
        default:
            ERR_UNEXPECTED_MODE
            return NULL;
    }

    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;
    paths->strand = strand;

    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 = trace;
    }
    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 = trace;
            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(int nA, int nB, Mode mode, unsigned char strand)
{
    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;
    paths->strand = strand;

    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].path = 0;
        switch (mode) {
            case Global:
                M[i][0].trace = 0;
                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;
                break;
            case Local:
                M[i][0].trace = STARTPOINT;
                break;
            default:
                ERR_UNEXPECTED_MODE
                return NULL;
        }
    }
    for (i = 1; i <= nB; i++) {
        switch (mode) {
            case Global:
                M[0][i].trace = 0;
                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;
                break;
            case Local:
                M[0][i].trace = STARTPOINT;
                break;
            default:
                ERR_UNEXPECTED_MODE
                return NULL;
        }
    }
    M[0][0].path = 0;
    return paths;
exit:
    Py_DECREF(paths);
    PyErr_SetNone(PyExc_MemoryError);
    return NULL;
}

static PathGenerator*
PathGenerator_create_FOGSAA(int nA, int nB, unsigned char strand)
{
    int i;
    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 = FOGSAA;
    paths->mode = FOGSAA_Mode;
    paths->length = 0;
    paths->strand = strand;

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


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

#define MATRIX_SCORE substitution_matrix[kA*n+kB]
#define COMPARE_SCORE (kA == wildcard || kB == wildcard) ? 0 : (kA == kB) ? match : mismatch


static PyObject*
Aligner_needlemanwunsch_score_compare(Aligner* self,
                                      const int* sA, int nA,
                                      const int* sB, int nB,
                                      unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    NEEDLEMANWUNSCH_SCORE(COMPARE_SCORE);
}

static PyObject*
Aligner_needlemanwunsch_score_matrix(Aligner* self,
                                     const int* sA, int nA,
                                     const int* sB, int nB,
                                     unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    NEEDLEMANWUNSCH_SCORE(MATRIX_SCORE);
}

static PyObject*
Aligner_smithwaterman_score_compare(Aligner* self,
                                    const int* sA, int nA,
                                    const int* sB, int nB)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    SMITHWATERMAN_SCORE(COMPARE_SCORE);
}

static PyObject*
Aligner_smithwaterman_score_matrix(Aligner* self,
                                   const int* sA, int nA,
                                   const int* sB, int nB)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    SMITHWATERMAN_SCORE(MATRIX_SCORE);
}

static PyObject*
Aligner_needlemanwunsch_align_compare(Aligner* self,
                                      const int* sA, int nA,
                                      const int* sB, int nB,
                                      unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    NEEDLEMANWUNSCH_ALIGN(COMPARE_SCORE);
}

static PyObject*
Aligner_needlemanwunsch_align_matrix(Aligner* self,
                                     const int* sA, int nA,
                                     const int* sB, int nB,
                                     unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    NEEDLEMANWUNSCH_ALIGN(MATRIX_SCORE);
}

static PyObject*
Aligner_smithwaterman_align_compare(Aligner* self,
                                    const int* sA, int nA,
                                    const int* sB, int nB,
                                    unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    SMITHWATERMAN_ALIGN(COMPARE_SCORE);
}

static PyObject*
Aligner_smithwaterman_align_matrix(Aligner* self,
                                   const int* sA, int nA,
                                   const int* sB, int nB,
                                   unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    SMITHWATERMAN_ALIGN(MATRIX_SCORE);
}

static PyObject*
Aligner_gotoh_global_score_compare(Aligner* self,
                                   const int* sA, int nA,
                                   const int* sB, int nB,
                                   unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    GOTOH_GLOBAL_SCORE(COMPARE_SCORE);
}

static PyObject*
Aligner_gotoh_global_score_matrix(Aligner* self,
                                  const int* sA, int nA,
                                  const int* sB, int nB,
                                  unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    GOTOH_GLOBAL_SCORE(MATRIX_SCORE);
}

static PyObject*
Aligner_gotoh_local_score_compare(Aligner* self,
                                  const int* sA, int nA,
                                  const int* sB, int nB)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    GOTOH_LOCAL_SCORE(COMPARE_SCORE);
}

static PyObject*
Aligner_gotoh_local_score_matrix(Aligner* self,
                                 const int* sA, int nA,
                                 const int* sB, int nB)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    GOTOH_LOCAL_SCORE(MATRIX_SCORE);
}

static PyObject*
Aligner_gotoh_global_align_compare(Aligner* self,
                                   const int* sA, int nA,
                                   const int* sB, int nB,
                                   unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    GOTOH_GLOBAL_ALIGN(COMPARE_SCORE);
}

static PyObject*
Aligner_gotoh_global_align_matrix(Aligner* self,
                                  const int* sA, int nA,
                                  const int* sB, int nB,
                                  unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    GOTOH_GLOBAL_ALIGN(MATRIX_SCORE);
}

static PyObject*
Aligner_gotoh_local_align_compare(Aligner* self,
                                  const int* sA, int nA,
                                  const int* sB, int nB,
                                  unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    GOTOH_LOCAL_ALIGN(COMPARE_SCORE);
}

static PyObject*
Aligner_gotoh_local_align_matrix(Aligner* self,
                                 const int* sA, int nA,
                                 const int* sB, int nB,
                                 unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    GOTOH_LOCAL_ALIGN(MATRIX_SCORE);
}

static int
_call_deletion_score_function(Aligner* aligner, int i, int j, int n, double* score)
{
    double value;
    PyObject* result;
    PyObject* function = aligner->deletion_score_function;
    if (!function) {
        if (i == 0) {
            value = aligner->open_left_deletion_score
                  + (j-1) * aligner->extend_left_deletion_score;
        }
        else if (i == n) {
            value = aligner->open_right_deletion_score
                  + (j-1) * aligner->extend_right_deletion_score;
        }
        else {
            value = aligner->open_internal_deletion_score
                  + (j-1) * aligner->extend_internal_deletion_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_insertion_score_function(Aligner* aligner, int i, int j, int n, double* score)
{
    double value;
    PyObject* result;
    PyObject* function = aligner->insertion_score_function;
    if (!function) {
        if (i == 0) {
            value = aligner->open_left_insertion_score
                  + (j-1) * aligner->extend_left_insertion_score;
        }
        else if (i == n) {
            value = aligner->open_right_insertion_score
                  + (j-1) * aligner->extend_right_insertion_score;
        }
        else {
            value = aligner->open_internal_insertion_score
                  + (j-1) * aligner->extend_internal_insertion_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_watermansmithbeyer_global_score_compare(Aligner* self,
                                                const int* sA, int nA,
                                                const int* sB, int nB,
                                                unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    WATERMANSMITHBEYER_ENTER_SCORE;
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_GLOBAL_SCORE(COMPARE_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_GLOBAL_SCORE(COMPARE_SCORE, nB-j);
            break;
	}
    }
    WATERMANSMITHBEYER_EXIT_SCORE;
}

static PyObject*
Aligner_watermansmithbeyer_global_score_matrix(Aligner* self,
                                               const int* sA, int nA,
                                               const int* sB, int nB,
                                               unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    WATERMANSMITHBEYER_ENTER_SCORE;
    switch (strand) {
        case '+':
            WATERMANSMITHBEYER_GLOBAL_SCORE(MATRIX_SCORE, j);
            break;
        case '-':
            WATERMANSMITHBEYER_GLOBAL_SCORE(MATRIX_SCORE, nB-j);
            break;
    }
    WATERMANSMITHBEYER_EXIT_SCORE;
}

static PyObject*
Aligner_watermansmithbeyer_local_score_compare(Aligner* self,
                                               const int* sA, int nA,
                                               const int* sB, int nB,
                                               unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    double maximum = 0.0;
    WATERMANSMITHBEYER_ENTER_SCORE;
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_LOCAL_SCORE(COMPARE_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_LOCAL_SCORE(COMPARE_SCORE, nB-j);
            break;
        }
    }
    WATERMANSMITHBEYER_EXIT_SCORE;
}

static PyObject*
Aligner_watermansmithbeyer_local_score_matrix(Aligner* self,
                                              const int* sA, int nA,
                                              const int* sB, int nB,
                                              unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    double maximum = 0.0;
    WATERMANSMITHBEYER_ENTER_SCORE;
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_LOCAL_SCORE(MATRIX_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_LOCAL_SCORE(MATRIX_SCORE, nB-j);
            break;
        }
    }
    WATERMANSMITHBEYER_EXIT_SCORE;
}

static PyObject*
Aligner_watermansmithbeyer_global_align_compare(Aligner* self,
                                                const int* sA, int nA,
                                                const int* sB, int nB,
                                                unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    WATERMANSMITHBEYER_ENTER_ALIGN(Global);
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_GLOBAL_ALIGN(COMPARE_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_GLOBAL_ALIGN(COMPARE_SCORE, nB-j);
            break;
	}
    }
    WATERMANSMITHBEYER_EXIT_ALIGN;
}

static PyObject*
Aligner_watermansmithbeyer_global_align_matrix(Aligner* self,
                                               const int* sA, int nA,
                                               const int* sB, int nB,
                                               unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    WATERMANSMITHBEYER_ENTER_ALIGN(Global);
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_GLOBAL_ALIGN(MATRIX_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_GLOBAL_ALIGN(MATRIX_SCORE, nB-j);
            break;
	}
    }
    WATERMANSMITHBEYER_EXIT_ALIGN;
}

static PyObject*
Aligner_watermansmithbeyer_local_align_compare(Aligner* self,
                                               const int* sA, int nA,
                                               const int* sB, int nB,
                                               unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    int im = nA;
    int jm = nB;
    double maximum = 0;
    WATERMANSMITHBEYER_ENTER_ALIGN(Local);
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_LOCAL_ALIGN(COMPARE_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_LOCAL_ALIGN(COMPARE_SCORE, nB-j);
            break;
	}
    }
    WATERMANSMITHBEYER_EXIT_ALIGN;
}

static PyObject*
Aligner_watermansmithbeyer_local_align_matrix(Aligner* self,
                                              const int* sA, int nA,
                                              const int* sB, int nB,
                                              unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    int im = nA;
    int jm = nB;
    double maximum = 0;
    WATERMANSMITHBEYER_ENTER_ALIGN(Local);
    switch (strand) {
        case '+': {
            WATERMANSMITHBEYER_LOCAL_ALIGN(MATRIX_SCORE, j);
            break;
        }
        case '-': {
            WATERMANSMITHBEYER_LOCAL_ALIGN(MATRIX_SCORE, nB-j);
            break;
	}
    }
    WATERMANSMITHBEYER_EXIT_ALIGN;
}

#define FOGSAA_CHECK_SCORES \
    if (mismatch >= match) { \
        PyObject *Bio_module = PyImport_ImportModule("Bio"); \
        PyObject *BiopythonWarning = PyObject_GetAttrString(Bio_module, "BiopythonWarning"); \
        Py_DECREF(Bio_module); \
        if (PyErr_WarnEx(BiopythonWarning, \
                    "Match score is less than mismatch score. Algorithm may return incorrect results.", 1)) { \
            Py_DECREF(BiopythonWarning); \
            return NULL; \
        } \
        Py_DECREF(BiopythonWarning); \
    } \
    if (    self->open_left_deletion_score > mismatch || \
            self->open_internal_deletion_score > mismatch || \
            self->open_right_deletion_score > mismatch || \
            self->open_left_insertion_score > mismatch || \
            self->open_internal_insertion_score > mismatch || \
            self->open_right_insertion_score > mismatch || \
            self->extend_left_deletion_score > mismatch || \
            self->extend_internal_deletion_score > mismatch || \
            self->extend_right_deletion_score > mismatch || \
            self->extend_left_insertion_score > mismatch || \
            self->extend_internal_insertion_score > mismatch || \
            self->extend_right_insertion_score > mismatch) { \
        PyObject *Bio_module = PyImport_ImportModule("Bio"); \
        PyObject *BiopythonWarning = PyObject_GetAttrString(Bio_module, "BiopythonWarning"); \
        Py_DECREF(Bio_module); \
        if (PyErr_WarnEx(BiopythonWarning, \
                    "One or more gap scores are greater than mismatch score. Algorithm may return incorrect results.", 1)) { \
            Py_DECREF(BiopythonWarning); \
            return NULL; \
        } \
        Py_DECREF(BiopythonWarning); \
    }

static PyObject*
Aligner_fogsaa_score_compare(Aligner* self,
                                 const int* sA, int nA,
                                 const int* sB, int nB,
                                 unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    FOGSAA_ENTER

    FOGSAA_CHECK_SCORES

    FOGSAA_DO(COMPARE_SCORE)
    FOGSAA_EXIT_SCORE
}

static PyObject*
Aligner_fogsaa_score_matrix(Aligner* self,
                                 const int* sA, int nA,
                                 const int* sB, int nB,
                                 unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    double match = substitution_matrix[0], mismatch = substitution_matrix[0];
    FOGSAA_ENTER

    // for prediction purposes, maximum score is match and minimum score is mismatch
    for (i = 0; i < n*n; i++) {
        if (substitution_matrix[i] > match)
            match = substitution_matrix[i];
        else if (substitution_matrix[i] < mismatch)
            mismatch = substitution_matrix[i];
    }
    FOGSAA_CHECK_SCORES

    FOGSAA_DO(MATRIX_SCORE)
    FOGSAA_EXIT_SCORE
}

static PyObject*
Aligner_fogsaa_align_compare(Aligner* self,
                                 const int* sA, int nA,
                                 const int* sB, int nB,
                                 unsigned char strand)
{
    const double match = self->match;
    const double mismatch = self->mismatch;
    const int wildcard = self->wildcard;
    PathGenerator* paths;
    Trace** M;
    FOGSAA_ENTER

    FOGSAA_CHECK_SCORES

    FOGSAA_DO(COMPARE_SCORE)
    FOGSAA_EXIT_ALIGN
}

static PyObject*
Aligner_fogsaa_align_matrix(Aligner* self,
                                 const int* sA, int nA,

                                 const int* sB, int nB,
                                 unsigned char strand)
{
    const Py_ssize_t n = self->substitution_matrix.shape[0];
    const double* substitution_matrix = self->substitution_matrix.buf;
    double match = substitution_matrix[0], mismatch = substitution_matrix[0];
    PathGenerator* paths;
    Trace** M;
    FOGSAA_ENTER

    // for prediction purposes, maximum score is match and minimum score is mismatch
    for (i = 0; i < n*n; i++) {
        if (substitution_matrix[i] > match)
            match = substitution_matrix[i];
        else if (substitution_matrix[i] < mismatch)
            mismatch = substitution_matrix[i];
    }
    FOGSAA_CHECK_SCORES

    FOGSAA_DO(MATRIX_SCORE)
    FOGSAA_EXIT_ALIGN
}

static bool _check_indices(Py_buffer* view, Py_buffer* substitution_matrix) {
    const Py_ssize_t m = substitution_matrix->shape[0];
    const int* indices = view->buf;
    const Py_ssize_t n = view->len / view->itemsize;
    Py_ssize_t i;
    for (i = 0; i < n; i++) {
        const int index = indices[i];
        if (index < 0) {
            PyErr_Format(PyExc_ValueError,
                         "sequence item %zd is negative (%d)",
                         i, index);
            return false;
        }
        if (index >= m) {
            PyErr_Format(PyExc_ValueError,
                         "sequence item %zd is out of bound"
                         " (%d, should be < %zd)", i, index, m);
            return false;
        }
    }
    return true;
}

static bool _map_indices(Py_buffer* view, const int* mapping, Py_ssize_t m) {
    Py_ssize_t i;
    const Py_ssize_t n = view->len / view->itemsize;
    int* const indices = view->buf;
    for (i = 0; i < n; i++) {
        int index = indices[i];
        if (index < 0) {
            PyErr_Format(PyExc_ValueError,
                         "sequence item %zd is negative (%d)",
                         i, index);
            return false;
        }
        if (index >= m) {
            PyErr_Format(PyExc_ValueError,
                         "sequence item %zd is out of bound"
                         " (%d, should be < %zd)", i, index, m);
            return false;
        }
        index = mapping[index];
        if (index == MISSING_LETTER) {
            PyErr_SetString(PyExc_ValueError,
                "sequence contains letters not in the alphabet");
            return false;
        }
        indices[i] = index;
    }
    return true;
}

static bool _prepare_indices(Py_buffer* substitution_matrix, Py_buffer* bA, Py_buffer* bB)
{
    if (PyObject_IsInstance(substitution_matrix->obj,
                            (PyObject*)Array_Type)) {
        const PyTypeObject* basetype = Array_Type->tp_base;
        const Py_ssize_t offset = basetype->tp_basicsize;
        Fields* fields = (Fields*)((intptr_t)substitution_matrix->obj + offset);
        Py_buffer* buffer = &fields->mapping;
        const int* mapping = buffer->buf;
        if (mapping) {
            const Py_ssize_t m = buffer->len / buffer->itemsize;
            if (!_map_indices(bA, mapping, m)) return false;
            if (!_map_indices(bB, mapping, m)) return false;
            return true;
        }
    }
    if (!_check_indices(bA, substitution_matrix)) return false;
    if (!_check_indices(bB, substitution_matrix)) return false;
    return true;
}

static int
sequence_converter(PyObject* argument, void* pointer)
{
    Py_buffer* view = pointer;
    const int flag = PyBUF_FORMAT | PyBUF_C_CONTIGUOUS;

    if (argument == NULL) {
        PyBuffer_Release(view);
        return 1;
    }

    if (PyObject_GetBuffer(argument, view, flag) != 0) {
        PyErr_SetString(PyExc_TypeError, "argument is not a sequence");
        return 0;
    }
    if (view->ndim != 1) {
        PyErr_Format(PyExc_ValueError,
                     "sequence has incorrect rank (%d expected 1)", view->ndim);
        PyBuffer_Release(view);
        return 0;
    }
    if (view->len == 0) {
        PyErr_SetString(PyExc_ValueError, "sequence has zero length");
        PyBuffer_Release(view);
        return 0;
    }
    if (strcmp(view->format, "i") != 0 && strcmp(view->format, "l") != 0) {
        PyErr_Format(PyExc_ValueError,
                     "sequence has incorrect data type '%s'", view->format);
        PyBuffer_Release(view);
        return 0;
    }
    if (view->itemsize != sizeof(int)) {
        PyErr_Format(PyExc_ValueError,
                    "sequence has unexpected item byte size "
                    "(%ld, expected %ld)", view->itemsize, sizeof(int));
        PyBuffer_Release(view);
        return 0;
    }
    return Py_CLEANUP_SUPPORTED;
}
 
static int
strand_converter(PyObject* argument, void* pointer)
{
    if (!PyUnicode_Check(argument)) goto error;
    if (PyUnicode_READY(argument) == -1) return 0;
    if (PyUnicode_GET_LENGTH(argument) == 1) {
        const Py_UCS4 ch = PyUnicode_READ_CHAR(argument, 0);
        if (ch < 128) {
            const char c = ch;
            if (ch == '+' || ch == '-') {
                *((char*)pointer) = c;
                return 1;
            }
        }
    }
error:
    PyErr_SetString(PyExc_ValueError, "strand must be '+' or '-'");
    return 0;
}

static const char Aligner_score__doc__[] = "calculates the alignment score";

static PyObject*
Aligner_score(Aligner* self, PyObject* args, PyObject* keywords)
{
    const int* sA;
    const int* sB;
    int nA;
    int nB;
    Py_buffer bA = {0};
    Py_buffer bB = {0};
    const Mode mode = self->mode;
    const Algorithm algorithm = _get_algorithm(self);
    char strand = '+';
    PyObject* result = NULL;
    PyObject* substitution_matrix = self->substitution_matrix.obj;

    static char *kwlist[] = {"sequenceA", "sequenceB", "strand", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, keywords, "O&O&O&", kwlist,
                                     sequence_converter, &bA,
                                     sequence_converter, &bB,
                                     strand_converter, &strand))
        return NULL;

    if (substitution_matrix) {
        if (!_prepare_indices(&self->substitution_matrix, &bA, &bB)) goto exit;
    }

    nA = (int) (bA.len / bA.itemsize);
    nB = (int) (bB.len / bB.itemsize);
    if (nA != bA.len / bA.itemsize || nB != bB.len / bB.itemsize) {
        PyErr_SetString(PyExc_ValueError, "sequences too long");
        goto exit;
    }
    sA = bA.buf;
    sB = bB.buf;

    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_needlemanwunsch_score_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_needlemanwunsch_score_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_smithwaterman_score_matrix(self, sA, nA, sB, nB);
                    else
                        result = Aligner_smithwaterman_score_compare(self, sA, nA, sB, nB);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case Gotoh:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_gotoh_global_score_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_gotoh_global_score_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_gotoh_local_score_matrix(self, sA, nA, sB, nB);
                    else
                        result = Aligner_gotoh_local_score_compare(self, sA, nA, sB, nB);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_watermansmithbeyer_global_score_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_watermansmithbeyer_global_score_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_watermansmithbeyer_local_score_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_watermansmithbeyer_local_score_compare(self, sA, nA, sB, nB, strand);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case FOGSAA:
            if (mode != FOGSAA_Mode) {
                ERR_UNEXPECTED_MODE
                goto exit;
            }
            if (substitution_matrix)
                result = Aligner_fogsaa_score_matrix(self, sA, nA, sB, nB, strand);
            else
                result = Aligner_fogsaa_score_compare(self, sA, nA, sB, nB, strand);
            break;
        case Unknown:
        default:
            ERR_UNEXPECTED_ALGORITHM
            break;
    }

exit:
    sequence_converter(NULL, &bA);
    sequence_converter(NULL, &bB);

    return result;
}

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

static PyObject*
Aligner_align(Aligner* self, PyObject* args, PyObject* keywords)
{
    const int* sA;
    const int* sB;
    int nA;
    int nB;
    Py_buffer bA = {0};
    Py_buffer bB = {0};
    const Mode mode = self->mode;
    const Algorithm algorithm = _get_algorithm(self);
    char strand = '+';
    PyObject* result = NULL;
    PyObject* substitution_matrix = self->substitution_matrix.obj;

    static char *kwlist[] = {"sequenceA", "sequenceB", "strand", NULL};

    if(!PyArg_ParseTupleAndKeywords(args, keywords, "O&O&O&", kwlist,
                                    sequence_converter, &bA,
                                    sequence_converter, &bB,
                                    strand_converter, &strand))
        return NULL;

    if (substitution_matrix) {
        if (!_prepare_indices(&self->substitution_matrix, &bA, &bB)) goto exit;
    }

    nA = (int) (bA.len / bA.itemsize);
    nB = (int) (bB.len / bB.itemsize);
    if (nA != bA.len / bA.itemsize || nB != bB.len / bB.itemsize) {
        PyErr_SetString(PyExc_ValueError, "sequences too long");
        goto exit;
    }
    sA = bA.buf;
    sB = bB.buf;

    switch (algorithm) {
        case NeedlemanWunschSmithWaterman:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_needlemanwunsch_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_needlemanwunsch_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_smithwaterman_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_smithwaterman_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case Gotoh:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_gotoh_global_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_gotoh_global_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_gotoh_local_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_gotoh_local_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case WatermanSmithBeyer:
            switch (mode) {
                case Global:
                    if (substitution_matrix)
                        result = Aligner_watermansmithbeyer_global_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_watermansmithbeyer_global_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                case Local:
                    if (substitution_matrix)
                        result = Aligner_watermansmithbeyer_local_align_matrix(self, sA, nA, sB, nB, strand);
                    else
                        result = Aligner_watermansmithbeyer_local_align_compare(self, sA, nA, sB, nB, strand);
                    break;
                default:
                    ERR_UNEXPECTED_MODE
                    goto exit;
            }
            break;
        case FOGSAA:
            if (mode != FOGSAA_Mode) {
                ERR_UNEXPECTED_MODE
                goto exit;
            }
            if (substitution_matrix)
                result = Aligner_fogsaa_align_matrix(self, sA, nA, sB, nB, strand);
            else
                result = Aligner_fogsaa_align_compare(self, sA, nA, sB, nB, strand);
            break;
        case Unknown:
        default:
            ERR_UNEXPECTED_ALGORITHM
            break;
    }

exit:
    sequence_converter(NULL, &bA);
    sequence_converter(NULL, &bB);

    return result;
}

static char Aligner_doc[] =
"The PairwiseAligner class implements common algorithms to align two\n"
"sequences to each other.\n";

static PyObject*
Aligner_warn_defaults_changed(Aligner* self)
// FIXME remove this function once Biopython release 1.87 is out
{
    if (warned)
        Py_RETURN_NONE;
    if (self->open_internal_insertion_score_set
     && self->extend_internal_insertion_score_set
     && self->open_left_insertion_score_set
     && self->extend_left_insertion_score_set
     && self->open_right_insertion_score_set
     && self->extend_right_insertion_score_set
     && self->open_internal_deletion_score_set
     && self->extend_internal_deletion_score_set
     && self->open_left_deletion_score_set
     && self->extend_left_deletion_score_set
     && self->open_right_deletion_score_set
     && self->extend_right_deletion_score_set) {
        Py_RETURN_NONE;
    }
    warned = true;
    PyErr_WarnEx(PyExc_UserWarning,
"\n"
"Note that the default value for the gap score parameter of a\n"
"PairwiseAligner object has changed.\n"
"\n"
"In older versions of Biopython, the pairwise aligner was initialized\n"
"by default with a match score of +1, a mismatch score of 0, and a gap\n"
"score of 0.  This choice was made to be consistent with the pairwise\n"
"alignment code in Bio.pairwise2.\n"
"\n"
"However, this scoring scheme tends to produce a large number of alignments\n"
"with only trivial difference between them.  In particular, a mismatch\n"
"between two letters, a single insertion followed by a deletion, and a\n"
"deletion followed by an insertion are all assigned the same score. For long\n"
"sequences, the number of alignments with such trivial differences can be\n"
"astronomical.\n"
"\n"
"In Biopython 1.86, the default gap score was therefore changed to -1,\n"
"while the default match score remained +1 and the default mismatch score\n"
"remained 0.\n", 1);
    Py_RETURN_NONE;
}

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__
    },
    {"warn_defaults_changed",
     (PyCFunction)Aligner_warn_defaults_changed,
     METH_NOARGS,
     "return False if all gap scores have been set explicitly, and True otherwise."
    },
    {NULL, NULL, 0, NULL}  /* Sentinel */
};

static PyTypeObject Aligner_Type = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "_pairwisealigner.PairwiseAligner",
    .tp_basicsize = sizeof(Aligner),
    .tp_dealloc = (destructor)Aligner_dealloc,
    .tp_repr = (reprfunc)Aligner_repr,
    .tp_str = (reprfunc)Aligner_str,
    .tp_flags =Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_doc = Aligner_doc,
    .tp_methods = Aligner_methods,
    .tp_getset = Aligner_getset,
    .tp_init = (initproc)Aligner_init,
};


/* Module definition */

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

static struct PyModuleDef moduledef = {
    PyModuleDef_HEAD_INIT,
    .m_name = "_pairwisealigner",
    .m_doc = _pairwisealigner__doc__,
    .m_size = -1,
};

PyObject *
PyInit__pairwisealigner(void)
{
    PyObject* module;
    Aligner_Type.tp_new = PyType_GenericNew;

    if (PyType_Ready(&Aligner_Type) < 0
     || PyType_Ready(&PathGenerator_Type) < 0)
        return NULL;

    module = PyModule_Create(&moduledef);
    if (!module) return NULL;

    Py_INCREF(&Aligner_Type);
    /* Reference to Aligner_Type will be stolen by PyModule_AddObject
     * only if it is successful. */
    if (PyModule_AddObject(module,
                           "PairwiseAligner", (PyObject*) &Aligner_Type) < 0) {
        Py_DECREF(&Aligner_Type);
        Py_DECREF(module);
        return NULL;
    }

    PyObject *mod = PyImport_ImportModule("Bio.Align.substitution_matrices._arraycore");
    if (!mod) {
        Py_DECREF(&Aligner_Type);
        Py_DECREF(module);
        return NULL;
    }

    Array_Type = (PyTypeObject*) PyObject_GetAttrString(mod, "Array");
    Py_DECREF(mod);

    if (!Array_Type) {
        Py_DECREF(&Aligner_Type);
        Py_DECREF(module);
        return NULL;
    }

    return module;
}
