module TestAlign

using Base.Test

using Bio
using Bio.Seq
using Bio.Align
using BGZFStreams
using TestFunctions
using YAML

# Generate a random valid alignment of a sequence of length n against a sequence
# of length m. If `glob` is true, generate a global alignment, if false, a local
# alignment.
function random_alignment(m, n, glob=true)
    match_ops = [OP_MATCH, OP_SEQ_MATCH, OP_SEQ_MISMATCH]
    insert_ops = [OP_INSERT, OP_SOFT_CLIP, OP_HARD_CLIP]
    delete_ops = [OP_DELETE, OP_SKIP]
    ops = vcat(match_ops, insert_ops, delete_ops)

    # This is just a random walk on a m-by-n matrix, where steps are either
    # (+1,0), (0,+1), (+1,+1). To make somewhat more realistic alignments, it's
    # biased towards going in the same direction. Local alignments have a random
    # start and end time, global alignments always start at (0,0) and end at
    # (m,n).

    # probability of choosing the same direction as the last step
    straight_pr = 0.9

    op = OP_MATCH
    if glob
        i = 0
        j = 0
        i_end = m
        j_end = n
    else
        i = rand(1:m-1)
        j = rand(1:n-1)
        i_end = rand(i+1:m)
        j_end = rand(j+1:n)
    end

    path = AlignmentAnchor[AlignmentAnchor(i, j, OP_START)]
    while (glob && i < i_end && j < j_end) || (!glob && (i < i_end || j < j_end))
        straight = rand() < straight_pr

        if i == i_end
            if !straight
                op = rand(delete_ops)
            end
            j += 1
        elseif j == j_end
            if !straight
                op = rand(inset_ops)
            end
            i += 1
        else
            if !straight
                op = rand(ops)
            end

            if isdeleteop(op)
                j += 1
            elseif isinsertop(op)
                i += 1
            else
                i += 1
                j += 1
            end
        end
        push!(path, AlignmentAnchor(i, j, op))
    end

    return path
end

# Make an Alignment from a path returned by random_alignment. Converting from
# path to Alignment is just done by removing redundant nodes from the path.
function anchors_from_path(path)
    anchors = AlignmentAnchor[]
    for k in 1:length(path)
        if k == length(path) || path[k].op != path[k+1].op
            push!(anchors, path[k])
        end
    end
    return anchors
end

# Generate a random range within `range`.
function randrange(range)
    x = rand(range)
    y = rand(range)
    if x < y
        return x:y
    else
        return y:x
    end
end

@testset "Alignments" begin
    @testset "Operations" begin
        @testset "Constructors and Conversions" begin
            ops = Set(Align.char_to_op)
            for op in ops
                if op != Align.OP_INVALID
                    @test Operation(Char(op)) == op
                end
            end
            @test_throws Exception Char(Align.OP_INVALID)
            @test_throws Exception Operation('m')
            @test_throws Exception Operation('7')
            @test_throws Exception Operation('A')
            @test_throws Exception Operation('\n')
        end
    end

    @testset "AlignmentAnchor" begin
        anchor = AlignmentAnchor(1, 2, OP_MATCH)
        @test string(anchor) == "AlignmentAnchor(1, 2, 'M')"
    end

    @testset "Alignment" begin
        # alignments with nonsense operations
        @test_throws Exception Alignment(AlignmentAnchor[
            Operation(0, 0, OP_START),
            Operation(100, 100, convert(Operation, 0xfa))])

        # test bad alignment anchors by swapping nodes in paths
        for _ in 1:100
            path = random_alignment(rand(1000:10000), rand(1000:10000))
            anchors = anchors_from_path(path)
            n = length(anchors)
            n < 3 && continue
            i = rand(2:n-1)
            j = rand(i+1:n)
            anchors[i], anchors[j] = anchors[j], anchors[i]
            @test_throws Exception Alignment(anchors)
        end

        # test bad alignment anchors by swapping operations
        for _ in 1:100
            path = random_alignment(rand(1000:10000), rand(1000:10000))
            anchors = anchors_from_path(path)
            n = length(anchors)
            n < 3 && continue
            i = rand(2:n-1)
            j = rand(i+1:n)
            u = anchors[i]
            v = anchors[j]
            if (ismatchop(u.op) && ismatchop(v.op)) ||
               (isinsertop(u.op) && isinsertop(v.op)) ||
               (isdeleteop(u.op) && isdeleteop(v.op))
                continue
            end
            anchors[i] = AlignmentAnchor(u.seqpos, u.refpos, v.op)
            anchors[j] = AlignmentAnchor(v.seqpos, v.refpos, u.op)
            @test_throws Exception Alignment(anchors)
        end

        # cigar string round-trip
        for _ in 1:100
            path = random_alignment(rand(1000:10000), rand(1000:10000))
            anchors = anchors_from_path(path)
            aln = Alignment(anchors)
            cig = cigar(aln)
            @test Alignment(cig, aln.anchors[1].seqpos + 1,
                            aln.anchors[1].refpos + 1) == aln
        end
    end

    @testset "AlignedSequence" begin
        #               0   4        9  12 15     19
        #               |   |        |  |  |      |
        #     query:     TGGC----ATCATTTAACG---CAAG
        # reference: AGGGTGGCATTTATCAG---ACGTTTCGAGAC
        #               |   |   |    |     |  |   |
        #               4   8   12   17    20 23  27
        anchors = [
            AlignmentAnchor( 0,  4, OP_START),
            AlignmentAnchor( 4,  8, OP_MATCH),
            AlignmentAnchor( 4, 12, OP_DELETE),
            AlignmentAnchor( 9, 17, OP_MATCH),
            AlignmentAnchor(12, 17, OP_INSERT),
            AlignmentAnchor(15, 20, OP_MATCH),
            AlignmentAnchor(15, 23, OP_DELETE),
            AlignmentAnchor(19, 27, OP_MATCH)
        ]
        query = "TGGCATCATTTAACGCAAG"
        alnseq = AlignedSequence(query, anchors)
        @test Bio.Align.first(alnseq) ==  5
        @test Bio.Align.last(alnseq)  == 27
        # OP_MATCH
        for (seqpos, refpos) in [(1, 5), (2, 6), (4, 8), (13, 18), (19, 27)]
            @test seq2ref(alnseq, seqpos) == (refpos, OP_MATCH)
            @test ref2seq(alnseq, refpos) == (seqpos, OP_MATCH)
        end
        # OP_INSERT
        @test seq2ref(alnseq, 10) == (17, OP_INSERT)
        @test seq2ref(alnseq, 11) == (17, OP_INSERT)
        # OP_DELETE
        @test ref2seq(alnseq,  9) == ( 4, OP_DELETE)
        @test ref2seq(alnseq, 10) == ( 4, OP_DELETE)
        @test ref2seq(alnseq, 23) == (15, OP_DELETE)

        seq = dna"ACGG--TGAAAGGT"
        ref = dna"-CGGGGA----TTT"
        alnseq = AlignedSequence(seq, ref)
        @test Bio.Align.first(alnseq) == 1
        @test Bio.Align.last(alnseq)  == 9
        @test alnseq.aln.anchors == [
             AlignmentAnchor( 0, 0, '0')
             AlignmentAnchor( 1, 0, 'I')
             AlignmentAnchor( 4, 3, '=')
             AlignmentAnchor( 4, 5, 'D')
             AlignmentAnchor( 5, 6, 'X')
             AlignmentAnchor( 9, 6, 'I')
             AlignmentAnchor(11, 8, 'X')
             AlignmentAnchor(12, 9, '=')
        ]
    end
end

# generate test cases from two aligned sequences
function alnscore{S,T}(::Type{S}, affinegap::AffineGapScoreModel{T}, alnstr::AbstractString, clip::Bool)
    gap_open = affinegap.gap_open
    gap_extend = affinegap.gap_extend
    lines = split(chomp(alnstr), '\n')
    a, b = lines[1:2]
    m = length(a)
    @assert m == length(b)

    if length(lines) == 2
        start = 1
        while start ≤ m && a[start] == ' ' || b[start] == ' '
            start += 1
        end
        stop = start
        while stop + 1 ≤ m && !(a[stop+1] == ' ' || b[stop+1] == ' ')
            stop += 1
        end
    elseif length(lines) == 3
        start = search(lines[3], '^')
        stop = rsearch(lines[3], '^')
    else
        error("invalid alignment string")
    end

    score = T(0)
    gap_extending_a = false
    gap_extending_b = false
    for i in start:stop
        if a[i] == '-'
            score += gap_extending_a ? gap_extend : (gap_open + gap_extend)
            gap_extending_a = true
        elseif b[i] == '-'
            score += gap_extending_b ? gap_extend : (gap_open + gap_extend)
            gap_extending_b = true
        else
            score += affinegap.submat[a[i],b[i]]
            gap_extending_a = false
            gap_extending_b = false
        end
    end
    sa = S(replace(a, r"\s|-", ""))
    sb = S(replace(b, r"\s|-", ""))
    return sa, sb, score, clip ? string(a[start:stop], '\n', b[start:stop]) : string(a, '\n', b)
end

function alnscore{T}(affinegap::AffineGapScoreModel{T}, alnstr::AbstractString; clip=true)
    return alnscore(AbstractString, affinegap, alnstr, clip)
end

function alndistance{S,T}(::Type{S}, cost::CostModel{T}, alnstr::AbstractString)
    lines = split(chomp(alnstr), '\n')
    @assert length(lines) == 2
    a, b = lines
    m = length(a)
    @assert length(b) == m
    dist = T(0)
    for i in 1:m
        if a[i] == '-'
            dist += cost.deletion
        elseif b[i] == '-'
            dist += cost.insertion
        else
            dist += cost.submat[a[i],b[i]]
        end
    end
    return S(replace(a, r"\s|-", "")), S(replace(b, r"\s|-", "")), dist
end

function alndistance{T}(cost::CostModel{T}, alnstr::AbstractString)
    return alndistance(AbstractString, cost, alnstr)
end

function alignedpair(alnres)
    aln = alignment(alnres)
    a = aln.a
    b = aln.b
    anchors = a.aln.anchors
    buf = IOBuffer()
    print_seq(buf, a, anchors)
    println(buf)
    print_ref(buf, b, anchors)
    return String(buf)
end

function print_seq(io, seq, anchors)
    for i in 2:length(anchors)
        if ismatchop(anchors[i].op) || isinsertop(anchors[i].op)
            for j in anchors[i-1].seqpos+1:anchors[i].seqpos
                print(io, seq.seq[j])
            end
        elseif isdeleteop(anchors[i].op)
            for _ in anchors[i-1].refpos+1:anchors[i].refpos
                print(io, '-')
            end
        end
    end
end

function print_ref(io, ref, anchors)
    for i in 2:length(anchors)
        if ismatchop(anchors[i].op) || isdeleteop(anchors[i].op)
            for j in anchors[i-1].refpos+1:anchors[i].refpos
                print(io, ref[j])
            end
        elseif isinsertop(anchors[i].op)
            for _ in anchors[i-1].seqpos+1:anchors[i].seqpos
                print(io, '-')
            end
        end
    end
end

@testset "PairwiseAlignment" begin
    @testset "SubstitutionMatrix" begin
        # DNA
        @test EDNAFULL[DNA_A,DNA_A] ===  5
        @test EDNAFULL[DNA_G,DNA_G] ===  5
        @test EDNAFULL[DNA_A,DNA_G] === -4
        @test EDNAFULL[DNA_G,DNA_A] === -4
        @test EDNAFULL[DNA_M,DNA_T] === -4
        @test EDNAFULL[DNA_M,DNA_C] ===  1

        # amino acid
        @test BLOSUM62[AA_A,AA_R] === -1
        @test BLOSUM62[AA_R,AA_A] === -1
        @test BLOSUM62[AA_R,AA_R] ===  5
        @test BLOSUM62[AA_O,AA_R] ===  0  # default
        @test BLOSUM62[AA_R,AA_O] ===  0  # default

        # update
        myblosum = copy(BLOSUM62)
        @test myblosum[AA_A,AA_R] === -1
        myblosum[AA_A,AA_R] = 10
        @test myblosum[AA_A,AA_R] === 10

        @test BLOSUM62[AA_O,AA_R] ===  0  # default
        myblosum[AA_O,AA_R] = -3
        @test myblosum[AA_O,AA_R] === -3

        submat = SubstitutionMatrix(DNANucleotide, rand(Float64, 15, 15))
        @test isa(submat, SubstitutionMatrix{DNANucleotide,Float64})

        submat = DichotomousSubstitutionMatrix(5, -4)
        @test isa(submat, DichotomousSubstitutionMatrix{Int})
        submat = convert(SubstitutionMatrix{DNANucleotide,Int}, submat)
        @test submat[DNA_A,DNA_A] ===  5
        @test submat[DNA_C,DNA_C] ===  5
        @test submat[DNA_A,DNA_C] === -4
        @test submat[DNA_C,DNA_A] === -4

        try
            print(IOBuffer(), EDNAFULL)
            print(IOBuffer(), BLOSUM62)
            # no error
            @test true
        catch
            @test false
        end
    end

    @testset "AffineGapScoreModel" begin
        # predefined substitution matrix
        for affinegap in [AffineGapScoreModel(BLOSUM62, -10, -1),
                          AffineGapScoreModel(BLOSUM62, gap_open=-10, gap_extend=-1),
                          AffineGapScoreModel(BLOSUM62, gap_open_penalty=10, gap_extend_penalty=1)]
            @test affinegap.gap_open == -10
            @test affinegap.gap_extend == -1
            @test typeof(affinegap) == AffineGapScoreModel{Int}
        end

        # matrix
        submat = SubstitutionMatrix(DNANucleotide, rand(Float64, 15, 15))
        for affinegap in [AffineGapScoreModel(submat, -3, -1),
                          AffineGapScoreModel(submat, gap_open=-3, gap_extend=-1),
                          AffineGapScoreModel(submat, gap_open_penalty=3, gap_extend_penalty=1)]
            @test affinegap.gap_open == -3
            @test affinegap.gap_extend == -1
            @test typeof(affinegap) == AffineGapScoreModel{Float64}
        end

        affinegap = AffineGapScoreModel(match=3, mismatch=-3, gap_open=-5, gap_extend=-2)
        @test affinegap.gap_open == -5
        @test affinegap.gap_extend == -2
        @test typeof(affinegap) == AffineGapScoreModel{Int}
    end

    @testset "CostModel" begin
        submat = SubstitutionMatrix(DNANucleotide, rand(Int, 15, 15))
        for cost in [CostModel(submat, 5, 6),
                     CostModel(submat, insertion=5, deletion=6)]
            @test cost.insertion == 5
            @test cost.deletion == 6
            @test typeof(cost) == CostModel{Int}
        end

        cost = CostModel(match=0, mismatch=3, insertion=5, deletion=6)
        @test cost.insertion == 5
        @test cost.deletion == 6
        @test typeof(cost) == CostModel{Int}
    end

    @testset "Alignment" begin
        anchors = [
            AlignmentAnchor(0, 0, OP_START),
            AlignmentAnchor(3, 3, OP_SEQ_MATCH)
        ]
        seq = AlignedSequence("ACG", anchors)
        ref = "ACG"
        aln = PairwiseAlignment(seq, ref)
        @test collect(aln) == [('A', 'A'), ('C', 'C'), ('G', 'G')]
        result = PairwiseAlignmentResult(3, true, seq, ref)
        @test isa(result, PairwiseAlignmentResult) == true
        @test isa(alignment(result), PairwiseAlignment) == true
        @test score(result) == 3
        @test hasalignment(result) == true
    end

    @testset "count_<ops>" begin
        # anchors are derived from an alignment:
        #   seq: ACG---TGCAGAATTT
        #        |     || || ||
        #   ref: AAAATTTGAAGTAT--
        a = dna"ACGTGCAGAATTT"
        b = dna"AAAATTTGAAGTAT"
        anchors = [
            AlignmentAnchor( 0,  0, '0'),
            AlignmentAnchor( 1,  1, '='),
            AlignmentAnchor( 3,  3, 'X'),
            AlignmentAnchor( 3,  6, 'D'),
            AlignmentAnchor( 5,  8, '='),
            AlignmentAnchor( 6,  9, 'X'),
            AlignmentAnchor( 8, 11, '='),
            AlignmentAnchor( 9, 12, 'X'),
            AlignmentAnchor(11, 14, '='),
            AlignmentAnchor(13, 14, 'I')
        ]
        aln = PairwiseAlignment(AlignedSequence(a, anchors), b)
        @test count_matches(aln) == 7
        @test count_mismatches(aln) == 4
        @test count_insertions(aln) == 2
        @test count_deletions(aln) == 3
        @test count_aligned(aln) == 16
    end

    @testset "Interfaces" begin
        seq = dna"ACGTATAGT"
        ref = dna"ATCGTATTGGT"
        # seq:  1 A-CGTATAG-T  9
        #         | ||||| | |
        # ref:  1 ATCGTATTGGT 11
        model = AffineGapScoreModel(EDNAFULL, gap_open=-4, gap_extend=-1)
        result = pairalign(GlobalAlignment(), seq, ref, model)
        @test isa(result, PairwiseAlignmentResult)
        aln = alignment(result)
        @test isa(aln, PairwiseAlignment)
        @test seq2ref(aln, 1) == (1, OP_SEQ_MATCH)
        @test seq2ref(aln, 2) == (3, OP_SEQ_MATCH)
        @test seq2ref(aln, 3) == (4, OP_SEQ_MATCH)
        @test ref2seq(aln, 1) == (1, OP_SEQ_MATCH)
        @test ref2seq(aln, 2) == (1, OP_DELETE)
        @test ref2seq(aln, 3) == (2, OP_SEQ_MATCH)
    end

    @testset "GlobalAlignment" begin
        affinegap = AffineGapScoreModel(
            match=0,
            mismatch=-6,
            gap_open=-5,
            gap_extend=-3
        )

        function testaln(alnstr)
            a, b, s, alnpair = alnscore(affinegap, alnstr)
            aln = pairalign(GlobalAlignment(), a, b, affinegap)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair
            aln = pairalign(GlobalAlignment(), a, b, affinegap, score_only=true)
            @test score(aln) == s
        end

        @testset "empty sequences" begin
            aln = pairalign(GlobalAlignment(), "", "", affinegap)
            @test score(aln) == 0
        end

        @testset "complete match" begin
            testaln("""
            ACGT
            ACGT
            """)
        end

        @testset "mismatch" begin
            testaln("""
            ACGT
            AGGT
            """)

            testaln("""
            ACGT
            AGGA
            """)
        end

        @testset "insertion" begin
            testaln("""
            ACGTT
            ACGT-
            """)

            testaln("""
            ACGTTT
            ACGT--
            """)

            testaln("""
            ACCGT
            AC-GT
            """)

            testaln("""
            ACCCGT
            AC--GT
            """)

            testaln("""
            AACGT
            A-CGT
            """)

            testaln("""
            AAACGT
            A--CGT
            """)
        end

        @testset "deletion" begin
            testaln("""
            ACGT-
            ACGTT
            """)

            testaln("""
            ACGT-
            ACGTT
            """)

            testaln("""
            ACGT--
            ACGTTT
            """)

            testaln("""
            AC-GT
            ACCGT
            """)

            testaln("""
            AC--GT
            ACCCGT
            """)

            testaln("""
            A-CGT
            AACGT
            """)

            testaln("""
            A--CGT
            AAACGT
            """)
        end

        @testset "banded" begin
            a, b, s, alnpair = alnscore(affinegap, """
            ACGT
            ACGT
            """)
            aln = pairalign(GlobalAlignment(), a, b, affinegap, banded=true)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair

            a, b, s, alnpair = alnscore(affinegap, """
            ACGT
            AGGT
            """)
            aln = pairalign(GlobalAlignment(), a, b, affinegap, banded=true)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair

            a, b, s, alnpair = alnscore(affinegap, """
            ACG--T
            ACGAAT
            """)
            aln = pairalign(GlobalAlignment(), a, b, affinegap, banded=true, lower_offset=0, upper_offset=0)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair
        end
    end

    @testset "SemiGlobalAlignment" begin
        affinegap = AffineGapScoreModel(
            match=0,
            mismatch=-6,
            gap_open=-5,
            gap_extend=-3
        )

        function testaln(alnstr)
            a, b, s, alnpair = alnscore(affinegap, alnstr, clip=false)
            aln = pairalign(SemiGlobalAlignment(), a, b, affinegap)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair
            aln = pairalign(SemiGlobalAlignment(), a, b, affinegap, score_only=true)
            @test score(aln) == s
        end

        @testset "complete match" begin
            testaln("""
            ACGT
            ACGT
            """)
        end

        @testset "partial match" begin
            testaln("""
            --ACTT---
            TTACGTAGT
              ^^^^
            """)

            testaln("""
            --AC-TTG-
            TTACGTTGT
              ^^^^^^
            """)

            testaln("""
            --ACTAGT---
            TTAC--GTTGT
              ^^^^^^
            """)
        end
    end

    @testset "OverlapAlignment" begin
        affinegap = AffineGapScoreModel(
            match=3,
            mismatch=-6,
            gap_open=-5,
            gap_extend=-3
        )

        function testaln(alnstr)
            a, b, s, alnpair = alnscore(affinegap, alnstr, clip=false)
            aln = pairalign(OverlapAlignment(), a, b, affinegap)
            @test score(aln) == s
            @test alignedpair(aln) == alnpair
            aln = pairalign(OverlapAlignment(), a, b, affinegap, score_only=true)
            @test score(aln) == s
        end

        @testset "complete match" begin
            testaln("""
            ACGT
            ACGT
            """)
        end

        @testset "partial match" begin
            testaln("""
            ---ACGGTGATTAT
            GATACGGTGA----
               ^^^^^^^
            """)

            testaln("""
            ---AACGT-GATTAT
            GATAACGGAGA----
               ^^^^^^^^
            """)

            testaln("""
            GATACGGTGA----
            ---ACGGTGATTAT
               ^^^^^^^
            """)

            testaln("""
            GATAACGGAGA----
            ---AACGT-GATTAT
               ^^^^^^^^
            """)
        end
    end

    @testset "LocalAlignment" begin
        @testset "zero matching score" begin
            affinegap = AffineGapScoreModel(
                match=0,
                mismatch=-6,
                gap_open=-5,
                gap_extend=-3
            )

            function testaln(alnstr)
                a, b, s, alnpair = alnscore(affinegap, alnstr)
                aln = pairalign(LocalAlignment(), a, b, affinegap)
                @test score(aln) == s
                @test alignedpair(aln) == alnpair
                aln = pairalign(LocalAlignment(), a, b, affinegap, score_only=true)
                @test score(aln) == s
            end

            @testset "empty sequences" begin
                aln = pairalign(LocalAlignment(), "", "", affinegap)
                @test score(aln) == 0
            end

            @testset "complete match" begin
                testaln("""
                ACGT
                ACGT
                """)
            end

            @testset "partial match" begin
                testaln("""
                ACGT
                AGGT
                  ^^
                """)

                testaln("""
                   ACGT
                AACGTTT
                      ^
                """)
            end

            @testset "no match" begin
                a = "AA"
                b = "TTTT"
                aln = pairalign(LocalAlignment(), a, b, affinegap)
                @test score(aln) == 0
            end
        end

        @testset "positive matching score" begin
            affinegap = AffineGapScoreModel(
                match=5,
                mismatch=-6,
                gap_open=-5,
                gap_extend=-3
            )

            function testaln(alnstr)
                a, b, s, alnpair = alnscore(affinegap, alnstr)
                aln = pairalign(LocalAlignment(), a, b, affinegap)
                @test score(aln) == s
                @test alignedpair(aln) == alnpair
                aln = pairalign(LocalAlignment(), a, b, affinegap, score_only=true)
                @test score(aln) == s
            end

            @testset "complete match" begin
                testaln("""
                ACGT
                ACGT
                ^^^^
                """)
            end

            @testset "partial match" begin
                testaln("""
                ACGT
                AGGT
                  ^^
                """)

                testaln("""
                 ACGT
                AACGTTT
                 ^^^^
                """)

                testaln("""
                  AC-GT
                AAACTGTTT
                """)
            end

            @testset "no match" begin
                a = "AA"
                b = "TTTT"
                aln = pairalign(LocalAlignment(), a, b, affinegap)
                @test score(aln) == 0
            end
        end
    end

    @testset "EditDistance" begin
        mismatch = 1
        submat = DichotomousSubstitutionMatrix(0, mismatch)
        insertion = 1
        deletion = 2
        cost = CostModel(submat, insertion, deletion)

        function testaln(alnstr)
            a, b, dist = alndistance(cost, alnstr)
            aln = pairalign(EditDistance(), a, b, cost)
            @test distance(aln) == dist
            @test alignedpair(aln) == chomp(alnstr)
            aln = pairalign(EditDistance(), a, b, cost, distance_only=true)
            @test distance(aln) == dist
        end

        @testset "empty sequences" begin
            aln = pairalign(EditDistance(), "", "", cost)
            @test distance(aln) == 0
        end

        @testset "complete match" begin
            testaln("""
            ACGT
            ACGT
            """)
        end

        @testset "mismatch" begin
            testaln("""
            AGGT
            ACGT
            """)

            testaln("""
            AGGT
            ACGT
            """)
        end

        @testset "insertion" begin
            testaln("""
            ACGTT
            ACG-T
            """)

            testaln("""
            ACGTT
            -CG-T
            """)
        end

        @testset "deletion" begin
            testaln("""
            AC-T
            ACGT
            """)

            testaln("""
            -C-T
            ACGT
            """)
        end
    end

    @testset "LevenshteinDistance" begin
        @testset "empty sequences" begin
            aln = pairalign(LevenshteinDistance(), "", "")
            @test distance(aln) == 0
        end

        @testset "complete match" begin
            a = "ACGT"
            b = "ACGT"
            aln = pairalign(LevenshteinDistance(), a, b)
            @test distance(aln) == 0
        end
    end

    @testset "HammingDistance" begin
        function testaln(alnstr)
            a, b = split(chomp(alnstr), '\n')
            dist = sum([x != y for (x, y) in zip(a, b)])
            aln = pairalign(HammingDistance(), a, b)
            @test distance(aln) == dist
            @test alignedpair(aln) == chomp(alnstr)
            aln = pairalign(HammingDistance(), a, b, distance_only=true)
            @test distance(aln) == dist
        end

        @testset "empty sequences" begin
            aln = pairalign(HammingDistance(), "", "")
            @test distance(aln) == 0
        end

        @testset "complete match" begin
            testaln("""
            ACGT
            ACGT
            """)
        end

        @testset "mismatch" begin
            testaln("""
            ACGT
            AGGT
            """)

            testaln("""
            ACGT
            AGGA
            """)
        end

        @testset "indel" begin
            @test_throws Exception pairalign(HammingDistance(), "ACGT", "ACG")
            @test_throws Exception pairalign(HammingDistance(), "ACG", "ACGT")
        end
    end
end

@testset "High-throughput Sequencing" begin
    get_bio_fmt_specimens()

    @testset "AuxDataDict" begin
        dict = AuxDataDict()
        @test length(dict) == 0
        @test isempty(dict)
        @test_throws KeyError dict["NM"]

        dict = AuxDataDict(
            "X1" => UInt8(1),
            "X2" => UInt16(2),
            "X3" => UInt32(3),
            "X4" => Int8(4),
            "X5" => Int16(5),
            "X6" => Int32(6),
            "X7" => Float32(7),
            "X8" => "eight",
            "X9" => Int32[9])
        @test length(dict) == 9
        @test !isempty(dict)
        @test dict["X1"] === UInt8(1)
        @test dict["X2"] === UInt16(2)
        @test dict["X3"] === UInt32(3)
        @test dict["X4"] === Int8(4)
        @test dict["X5"] === Int16(5)
        @test dict["X6"] === Int32(6)
        @test dict["X7"] === Float32(7)
        @test dict["X8"] == "eight"
        @test typeof(dict["X8"]) == String
        @test dict["X9"] == Int32[9]
        @test typeof(dict["X9"]) == Vector{Int32}

        dict = AuxDataDict("NM" => 0x01, "XY" => Int32(100), "XZ" => [0x11, 0x23])
        @test length(dict) == 3
        @test dict["NM"] === 0x01
        @test dict["XY"] === Int32(100)
        @test dict["XZ"] == [0x11, 0x23]
        @test eltype(dict["XZ"]) == UInt8

        dict = AuxDataDict("NM" => 0x01, "MD" => "8T1T39")
        @test length(dict) == 2
        @test dict["NM"] === 0x01
        @test dict["MD"] == "8T1T39"
        dict["NM"] = 0x00
        @test dict["NM"] === 0x00
        dict["MD"] = "50"
        @test dict["MD"] == "50"
        @test collect(dict) == ["NM" => 0x00, "MD" => "50"]
        dict["XY"] = "foobar"
        @test dict["XY"] == "foobar"
        @test collect(dict) == ["NM" => 0x00, "MD" => "50", "XY" => "foobar"]
        delete!(dict, "NM")
        @test length(dict) == 2
        @test collect(dict) == ["MD" => "50", "XY" => "foobar"]
    end

    @testset "SAM" begin
        samdir = Pkg.dir("Bio", "test", "BioFmtSpecimens", "SAM")

        @testset "SAMHeader" begin
            h = SAMHeader()
            @test isa(h, Associative)
            @test isempty(h)
            h["HD"] = Dict("VN" => "100.100", "SO" => "unknown")
            @test length(h) == 1
            @test h["HD"]["VN"] == "100.100"
            h["CO"] = ["comment1", "comment2"]
            @test length(h) == 2
            @test h["CO"] == ["comment1", "comment2"]
            delete!(h, "CO")
            @test length(h) == 1
        end

        @testset "Record" begin
            rec = SAMRecord()
            @test !ismapped(rec)

            # default values
            @test seqname(rec) == "*"
            @test flag(rec) == SAM_FLAG_UNMAP
            @test refname(rec) == "*"
            @test leftposition(rec) == 0
            @test rightposition(rec) == -1
            @test nextrefname(rec) == "*"
            @test nextleftposition(rec) == 0
            @test templatelength(rec) == 0
            @test cigar(rec) == "*"
            @test alignment(rec) == Alignment(AlignmentAnchor[])
            @test sequence(rec) == "*"
            @test_throws ArgumentError seqlength(rec)
            @test qualities(rec) == "*"
            @test Align.alignment_length(rec) === 0

            buf = IOBuffer()
            show(buf, rec)
            @test startswith(takebuf_string(buf), "Bio.Align.SAMRecord:")

            buf = IOBuffer()
            showcompact(buf, rec)
            @test takebuf_string(buf) == "*\tunmapped"

            # set & delete tags
            rec = SAMRecord()
            @test !haskey(rec, "MN")
            rec["MN"] = 0x01
            @test rec["MN"] === 0x01
            @test haskey(rec, "MN")
            @test !haskey(rec, "XY")
            rec["XY"] = "foobar"
            @test rec["XY"] == "foobar"
            @test haskey(rec, "XY")
            delete!(rec, "MN")
            @test !haskey(rec, "MN")
        end

        @testset "Reader" begin
            reader = open(SAMReader, joinpath(samdir, "ce#1.sam"))
            @test isa(reader, SAMReader)
            @test eltype(reader) === SAMRecord
            @test startswith(repr(reader), "Bio.Align.SAMReader:")

            # header
            h = header(reader)
            @test h["SQ"] == [Dict("SN" => "CHROMOSOME_I", "LN" => "1009800")]

            # first record
            rec = SAMRecord()
            read!(reader, rec)
            @test ismapped(rec)
            @test refname(rec) == "CHROMOSOME_I"
            @test leftposition(rec) == 2
            @test rightposition(rec) == 102
            @test seqname(rec) == "SRR065390.14978392"
            @test sequence(rec)  == "CCTAGCCCTAACCCTAACCCTAACCCTAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAA"
            @test seqlength(rec) == 100
            @test qualities(rec) == "#############################@B?8B?BA@@DDBCDDCBC@CDCDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
            @test flag(rec) == 16
            @test cigar(rec) == "27M1D73M"
            @test alignment(rec) == Alignment([
                AlignmentAnchor(  0,   1, OP_START),
                AlignmentAnchor( 27,  28, OP_MATCH),
                AlignmentAnchor( 27,  29, OP_DELETE),
                AlignmentAnchor(100, 102, OP_MATCH)])
            @test rec["XG"] == 1
            @test rec["XM"] == 5
            @test rec["XN"] == 0
            @test rec["XO"] == 1
            @test rec["AS"] == -18
            @test rec["XS"] == -18
            @test rec["YT"] == "UU"
            @test eof(reader)
            close(reader)

            # iterator
            @test length(collect(open(SAMReader, joinpath(samdir, "ce#1.sam")))) == 1
            @test length(collect(open(SAMReader, joinpath(samdir, "ce#2.sam")))) == 2

            # IOStream
            @test length(collect(SAMReader(open(joinpath(samdir, "ce#1.sam"))))) == 1
            @test length(collect(SAMReader(open(joinpath(samdir, "ce#2.sam"))))) == 2
        end

        @testset "Round trip" begin
            for specimen in YAML.load_file(joinpath(samdir, "index.yml"))
                filepath = joinpath(samdir, specimen["filename"])
                mktemp() do path, io
                    # copy
                    reader = open(SAMReader, filepath)
                    writer = SAMWriter(io, header(reader))
                    records = SAMRecord[]
                    for rec in reader
                        push!(records, rec)
                        write(writer, rec)
                    end
                    close(reader)
                    close(writer)

                    # read again
                    reader = open(SAMReader, path)
                    @test collect(reader) == records
                    close(reader)
                end
            end
        end
    end

    @testset "BAM" begin
        bamdir = Pkg.dir("Bio", "test", "BioFmtSpecimens", "BAM")

        @testset "Record" begin
            rec = BAMRecord()
            @test !ismapped(rec)

            # default values
            @test refname(rec) == "*"
            @test refindex(rec) == 0
            @test leftposition(rec) == 0
            @test rightposition(rec) == -1
            @test mappingquality(rec) == 0
            @test flag(rec) == SAM_FLAG_UNMAP
            @test nextrefname(rec) == "*"
            @test nextrefindex(rec) == 0
            @test nextleftposition(rec) == 0
            @test templatelength(rec) == 0
            @test seqname(rec) == ""
            @test cigar(rec) == ""
            @test sequence(rec) == dna""
            @test seqlength(rec) == 0
            @test alignment(rec) == Alignment(AlignmentAnchor[])
            @test qualities(rec) == UInt8[]
            @test Align.alignment_length(rec) === 0

            buf = IOBuffer()
            show(buf, rec)
            @test startswith(takebuf_string(buf), "Bio.Align.BAMRecord:")

            buf = IOBuffer()
            showcompact(buf, rec)
            @test takebuf_string(buf) == "(empty name)\tunmapped"

            # set & delete tags
            rec = BAMRecord()
            @test !haskey(rec, "MN")
            rec["MN"] = 0x01
            @test rec["MN"] === 0x01
            @test haskey(rec, "MN")
            @test !haskey(rec, "XY")
            rec["XY"] = "foobar"
            @test rec["XY"] == "foobar"
            @test haskey(rec, "XY")
            delete!(rec, "MN")
            @test !haskey(rec, "MN")
        end

        @testset "Reader" begin
            reader = open(BAMReader, joinpath(bamdir, "ce#1.bam"))
            @test isa(reader, BAMReader)
            @test eltype(reader) === BAMRecord
            @test startswith(repr(reader), "Bio.Align.BAMReader{IOStream}:")

            # header
            h = header(reader)
            @test h["SQ"] == [Dict("SN" => "CHROMOSOME_I", "LN" => "1009800")]

            # first record
            rec = BAMRecord()
            read!(reader, rec)
            @test ismapped(rec)
            @test refname(rec) == "CHROMOSOME_I"
            @test refindex(rec) == 1
            @test leftposition(rec) == 2
            @test rightposition(rec) == 102
            @test seqname(rec) == "SRR065390.14978392"
            @test sequence(rec) == dna"""
            CCTAGCCCTAACCCTAACCCTAACCCTAGCCTAAGCCTAAGCCTAAGCCT
            AAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAAGCCTAA
            """
            @test seqlength(rec) == 100
            @test eltype(qualities(rec)) == Int8
            @test qualities(rec) == [Int(x) - 33 for x in "#############################@B?8B?BA@@DDBCDDCBC@CDCDCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"]
            @test flag(rec) == 16
            @test cigar(rec) == "27M1D73M"
            @test alignment(rec) == Alignment([
                AlignmentAnchor(  0,   1, OP_START),
                AlignmentAnchor( 27,  28, OP_MATCH),
                AlignmentAnchor( 27,  29, OP_DELETE),
                AlignmentAnchor(100, 102, OP_MATCH)])
            @test rec["XG"] == 1
            @test rec["XM"] == 5
            @test rec["XN"] == 0
            @test rec["XO"] == 1
            @test rec["AS"] == -18
            @test rec["XS"] == -18
            @test rec["YT"] == "UU"
            @test eof(reader)
            close(reader)

            # iterator
            @test length(collect(open(BAMReader, joinpath(bamdir, "ce#1.bam")))) == 1
            @test length(collect(open(BAMReader, joinpath(bamdir, "ce#2.bam")))) == 2

            # IOStream
            @test length(collect(BAMReader(open(joinpath(bamdir, "ce#1.bam"))))) == 1
            @test length(collect(BAMReader(open(joinpath(bamdir, "ce#2.bam"))))) == 2
        end

        @testset "Round trip" begin
            for specimen in YAML.load_file(joinpath(bamdir, "index.yml"))
                filepath = joinpath(bamdir, specimen["filename"])
                mktemp() do path, _
                    # copy
                    reader = open(BAMReader, filepath)
                    writer = BAMWriter(
                        BGZFStream(path, "w"),
                        header(reader, true))
                    records = BAMRecord[]
                    for rec in reader
                        push!(records, rec)
                        write(writer, rec)
                    end
                    close(reader)
                    close(writer)

                    # read again
                    reader = open(BAMReader, path)
                    @test collect(reader) == records
                    close(reader)
                end
            end
        end

        @testset "Random access" begin
            filepath = joinpath(bamdir, "GSE25840_GSM424320_GM06985_gencode_spliced.head.bam")
            reader = open(BAMReader, filepath, index=filepath * ".bai")

            # expected values are counted using samtools
            for (refname, interval, expected) in [
                    ("chr1", 1000:10000,      21),
                    ("chr1", 8000:10000,      20),
                    ("chr1", 766_000:800_000, 142),
                    ("chr1", 786_000:800_000, 1),
                    ("chr1", 796_000:800_000, 0)]
                intsect = intersect(reader, refname, interval)
                @test eltype(intsect) == BAMRecord
                @test count(_ -> true, intsect) == expected
                # check that the intersection iterator is stateless
                @test count(_ -> true, intsect) == expected
            end

            # randomized tests
            for n in 1:50
                refindex = 1
                refname = "chr1"
                range = randrange(1:1_000_000)
                seekstart(reader)
                # linear scan
                expected = collect(filter(reader) do rec
                    isoverlapping(rec, refindex, range)
                end)
                # indexed scan
                actual = collect(intersect(reader, refname, range))
                @test actual == expected
            end

            close(reader)
        end
    end
end

end # TestAlign


# Copyright (c) 2013: Iain Dunning, Miles Lubin, and contributors
#
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

mutable struct IpoptProblem
    ipopt_problem::Ptr{Cvoid}   # Reference to the internal data structure
    n::Int                      # Num vars
    m::Int                      # Num cons
    x::Vector{Float64}          # Starting and final solution
    g::Vector{Float64}          # Final constraint values
    mult_g::Vector{Float64}     # lagrange multipliers on constraints
    mult_x_L::Vector{Float64}   # lagrange multipliers on lower bounds
    mult_x_U::Vector{Float64}   # lagrange multipliers on upper bounds
    obj_val::Float64            # Final objective
    status::Cint                # Final status
    # Callbacks
    eval_f::Function
    eval_g::Function
    eval_grad_f::Function
    eval_jac_g::Function
    eval_h::Union{Function,Nothing}
    intermediate::Union{Function,Nothing}
end

function _Eval_F_CB(
    n::Cint,
    x_ptr::Ptr{Float64},
    x_new::Cint,
    obj_value::Ptr{Float64},
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    x = unsafe_wrap(Array, x_ptr, Int(n))
    if x_new == Cint(1)
        prob.x .= x
    end
    new_obj = convert(Float64, prob.eval_f(x))::Float64
    unsafe_store!(obj_value, new_obj)
    return Cint(1)
end

function _Eval_Grad_F_CB(
    n::Cint,
    x_ptr::Ptr{Float64},
    # A Bool indicating if `x` is a new point. We don't make use of this.
    ::Cint,
    grad_f::Ptr{Float64},
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    new_grad_f = unsafe_wrap(Array, grad_f, Int(n))
    x = unsafe_wrap(Array, x_ptr, Int(n))
    prob.eval_grad_f(x, new_grad_f)
    return Cint(1)
end

function _Eval_G_CB(
    n::Cint,
    x_ptr::Ptr{Float64},
    x_new::Cint,
    m::Cint,
    g_ptr::Ptr{Float64},
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    new_g = unsafe_wrap(Array, g_ptr, Int(m))
    x = unsafe_wrap(Array, x_ptr, Int(n))
    if x_new == Cint(1)
        prob.x .= x
    end
    prob.eval_g(x, new_g)
    return Cint(1)
end

function _Eval_Jac_G_CB(
    n::Cint,
    x_ptr::Ptr{Float64},
    ::Cint,
    ::Cint,
    nele_jac::Cint,
    iRow::Ptr{Cint},
    jCol::Ptr{Cint},
    values_ptr::Ptr{Float64},
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    x = unsafe_wrap(Array, x_ptr, Int(n))
    rows = unsafe_wrap(Array, iRow, Int(nele_jac))
    cols = unsafe_wrap(Array, jCol, Int(nele_jac))
    if values_ptr == C_NULL
        prob.eval_jac_g(x, rows, cols, nothing)
    else
        values = unsafe_wrap(Array, values_ptr, Int(nele_jac))
        prob.eval_jac_g(x, rows, cols, values)
    end
    return Cint(1)
end

function _Eval_H_CB(
    n::Cint,
    x_ptr::Ptr{Float64},
    ::Cint,
    obj_factor::Float64,
    m::Cint,
    lambda_ptr::Ptr{Float64},
    ::Cint,
    nele_hess::Cint,
    iRow::Ptr{Cint},
    jCol::Ptr{Cint},
    values_ptr::Ptr{Float64},
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    if prob.eval_h === nothing
        # No hessian. Return FALSE for failure.
        return Cint(0)
    end
    x = unsafe_wrap(Array, x_ptr, Int(n))
    lambda = unsafe_wrap(Array, lambda_ptr, Int(m))
    rows = unsafe_wrap(Array, iRow, Int(nele_hess))
    cols = unsafe_wrap(Array, jCol, Int(nele_hess))
    if values_ptr == C_NULL
        prob.eval_h(x, rows, cols, obj_factor, lambda, nothing)
    else
        values = unsafe_wrap(Array, values_ptr, Int(nele_hess))
        prob.eval_h(x, rows, cols, obj_factor, lambda, values)
    end
    return Cint(1)  # Return TRUE for success.
end

function _Intermediate_CB(
    alg_mod::Cint,
    iter_count::Cint,
    obj_value::Float64,
    inf_pr::Float64,
    inf_du::Float64,
    mu::Float64,
    d_norm::Float64,
    regularization_size::Float64,
    alpha_du::Float64,
    alpha_pr::Float64,
    ls_trials::Cint,
    user_data::Ptr{Cvoid},
)
    prob = unsafe_pointer_to_objref(user_data)::IpoptProblem
    ret = prob.intermediate(
        alg_mod,
        iter_count,
        obj_value,
        inf_pr,
        inf_du,
        mu,
        d_norm,
        regularization_size,
        alpha_du,
        alpha_pr,
        ls_trials,
    )
    # Return TRUE if the optimization should continue.
    return ret ? Cint(1) : Cint(0)
end

function CreateIpoptProblem(
    n::Int,
    x_L::Vector{Float64},
    x_U::Vector{Float64},
    m::Int,
    g_L::Vector{Float64},
    g_U::Vector{Float64},
    nele_jac::Int,
    nele_hess::Int,
    eval_f,
    eval_g,
    eval_grad_f,
    eval_jac_g,
    eval_h,
)
    @assert n == length(x_L) == length(x_U)
    @assert m == length(g_L) == length(g_U)
    eval_f_cb = @cfunction(
        _Eval_F_CB,
        Cint,
        (Cint, Ptr{Float64}, Cint, Ptr{Float64}, Ptr{Cvoid}),
    )
    eval_g_cb = @cfunction(
        _Eval_G_CB,
        Cint,
        (Cint, Ptr{Float64}, Cint, Cint, Ptr{Float64}, Ptr{Cvoid}),
    )
    eval_grad_f_cb = @cfunction(
        _Eval_Grad_F_CB,
        Cint,
        (Cint, Ptr{Float64}, Cint, Ptr{Float64}, Ptr{Cvoid}),
    )
    eval_jac_g_cb = @cfunction(
        _Eval_Jac_G_CB,
        Cint,
        (
            Cint,
            Ptr{Float64},
            Cint,
            Cint,
            Cint,
            Ptr{Cint},
            Ptr{Cint},
            Ptr{Float64},
            Ptr{Cvoid},
        ),
    )
    eval_h_cb = @cfunction(
        _Eval_H_CB,
        Cint,
        (
            Cint,
            Ptr{Float64},
            Cint,
            Float64,
            Cint,
            Ptr{Float64},
            Cint,
            Cint,
            Ptr{Cint},
            Ptr{Cint},
            Ptr{Float64},
            Ptr{Cvoid},
        ),
    )
    ipopt_problem = ccall(
        (:CreateIpoptProblem, libipopt),
        Ptr{Cvoid},
        (
            Cint,
            Ptr{Float64},
            Ptr{Float64},
            Cint,
            Ptr{Float64},
            Ptr{Float64},
            Cint,
            Cint,
            Cint,
            Ptr{Cvoid},
            Ptr{Cvoid},
            Ptr{Cvoid},
            Ptr{Cvoid},
            Ptr{Cvoid},
        ),
        n,
        x_L,
        x_U,
        m,
        g_L,
        g_U,
        nele_jac,
        nele_hess,
        1,  # 1 = Fortran style indexing
        eval_f_cb,
        eval_g_cb,
        eval_grad_f_cb,
        eval_jac_g_cb,
        eval_h_cb,
    )
    if ipopt_problem == C_NULL
        if n == 0
            error(
                "IPOPT: Failed to construct problem because there are 0 " *
                "variables. If you intended to construct an empty problem, " *
                "one work-around is to add a variable fixed to 0.",
            )
        else
            error("IPOPT: Failed to construct problem for some unknown reason.")
        end
    end
    prob = IpoptProblem(
        ipopt_problem,
        n,
        m,
        zeros(Float64, n),
        zeros(Float64, m),
        zeros(Float64, m),
        zeros(Float64, n),
        zeros(Float64, n),
        0.0,
        0,
        eval_f,
        eval_g,
        eval_grad_f,
        eval_jac_g,
        eval_h,
        nothing,
    )
    finalizer(FreeIpoptProblem, prob)
    return prob
end

function FreeIpoptProblem(prob::IpoptProblem)
    ccall(
        (:FreeIpoptProblem, libipopt),
        Cvoid,
        (Ptr{Cvoid},),
        prob.ipopt_problem,
    )
    return
end

function AddIpoptStrOption(prob::IpoptProblem, keyword::String, value::String)
    if !(isascii(keyword) && isascii(value))
        error("IPOPT: Non ASCII parameters not supported")
    end
    ret = ccall(
        (:AddIpoptStrOption, libipopt),
        Cint,
        (Ptr{Cvoid}, Ptr{UInt8}, Ptr{UInt8}),
        prob.ipopt_problem,
        keyword,
        value,
    )
    if ret == 0
        error("IPOPT: Couldn't set option '$keyword' to value '$value'.")
    end
    return
end

function AddIpoptNumOption(prob::IpoptProblem, keyword::String, value::Float64)
    if !isascii(keyword)
        error("IPOPT: Non ASCII parameters not supported")
    end
    ret = ccall(
        (:AddIpoptNumOption, libipopt),
        Cint,
        (Ptr{Cvoid}, Ptr{UInt8}, Float64),
        prob.ipopt_problem,
        keyword,
        value,
    )
    if ret == 0
        error("IPOPT: Couldn't set option '$keyword' to value '$value'.")
    end
    return
end

function AddIpoptIntOption(prob::IpoptProblem, keyword::String, value::Integer)
    if !isascii(keyword)
        error("IPOPT: Non ASCII parameters not supported")
    end
    ret = ccall(
        (:AddIpoptIntOption, libipopt),
        Cint,
        (Ptr{Cvoid}, Ptr{UInt8}, Cint),
        prob.ipopt_problem,
        keyword,
        value,
    )
    if ret == 0
        error("IPOPT: Couldn't set option '$keyword' to value '$value'.")
    end
    return
end

function OpenIpoptOutputFile(
    prob::IpoptProblem,
    file_name::String,
    print_level::Int,
)
    if !isascii(file_name)
        error("IPOPT: Non ASCII parameters not supported")
    end
    ret = ccall(
        (:OpenIpoptOutputFile, libipopt),
        Cint,
        (Ptr{Cvoid}, Ptr{UInt8}, Cint),
        prob.ipopt_problem,
        file_name,
        print_level,
    )
    if ret == 0
        error("IPOPT: Couldn't open output file.")
    end
    return
end

function SetIpoptProblemScaling(
    prob::IpoptProblem,
    obj_scaling::Float64,
    x_scaling::Union{Ptr{Cvoid},Vector{Float64}},
    g_scaling::Union{Ptr{Cvoid},Vector{Float64}},
)
    ret = ccall(
        (:SetIpoptProblemScaling, libipopt),
        Cint,
        (Ptr{Cvoid}, Float64, Ptr{Float64}, Ptr{Float64}),
        prob.ipopt_problem,
        obj_scaling,
        x_scaling,
        g_scaling,
    )
    if ret == 0
        error("IPOPT: Error setting problem scaling.")
    end
    return
end

function SetIntermediateCallback(prob::IpoptProblem, intermediate::Function)
    intermediate_cb = @cfunction(
        _Intermediate_CB,
        Cint,
        (
            Cint,
            Cint,
            Float64,
            Float64,
            Float64,
            Float64,
            Float64,
            Float64,
            Float64,
            Float64,
            Cint,
            Ptr{Cvoid},
        ),
    )
    ret = ccall(
        (:SetIntermediateCallback, libipopt),
        Cint,
        (Ptr{Cvoid}, Ptr{Cvoid}),
        prob.ipopt_problem,
        intermediate_cb,
    )
    if ret == 0
        error("IPOPT: Something went wrong setting the intermediate callback.")
    end
    prob.intermediate = intermediate
    return
end

function IpoptSolve(prob::IpoptProblem)
    final_objval = Ref{Cdouble}(0.0)
    ret = ccall(
        (:IpoptSolve, libipopt),
        Cint,
        (
            Ptr{Cvoid},
            Ptr{Float64},
            Ptr{Float64},
            Ptr{Float64},
            Ptr{Float64},
            Ptr{Float64},
            Ptr{Float64},
            Ptr{Cvoid},
        ),
        prob.ipopt_problem,
        prob.x,
        prob.g,
        final_objval,
        prob.mult_g,
        prob.mult_x_L,
        prob.mult_x_U,
        pointer_from_objref(prob),
    )
    prob.obj_val = final_objval[]
    prob.status = ret
    return prob.status
end


"""
Reaction struct.

# Fields
````
id :: String
name :: String
metabolites :: Dict{Metabolite, Float64}
lb :: Float64
ub :: Float64
grr :: Vector{Vector{Gene}}
subsystem :: String
notes :: Dict{String, Vector{String}}
annotation :: Dict{String, Union{Vector{String}, String}}
objective_coefficient :: Float64
````
"""
mutable struct Reaction
    id::String
    name::Maybe{String}
    metabolites::Dict{String,Float64}
    lb::Float64
    ub::Float64
    grr::Maybe{GeneAssociation}
    subsystem::Maybe{String}
    notes::Notes
    annotations::Annotations
    objective_coefficient::Float64

    Reaction(
        id = "";
        name = nothing,
        metabolites = Dict{String,Float64}(),
        lb = -_constants.default_reaction_bound,
        ub = _constants.default_reaction_bound,
        grr = nothing,
        subsystem = nothing,
        notes = Notes(),
        annotations = Annotations(),
        objective_coefficient = 0.0,
    ) = new(
        id,
        name,
        metabolites,
        lb,
        ub,
        grr,
        subsystem,
        notes,
        annotations,
        objective_coefficient,
    )

    # this constructor is only used internally
    function Reaction(
        id::String,
        metabolites::Dict{String,Float64},
        dir = :bidirectional;
        default_bound = _constants.default_reaction_bound,
    )
        if dir == :forward
            lb = 0.0
            ub = default_bound
        elseif dir == :reverse
            lb = -default_bound
            ub = 0.0
        else
            lb = -default_bound
            ub = default_bound
        end
        Reaction(id; metabolites = metabolites, lb = lb, ub = ub)
    end
end


module Taps
#import Base.getproperty, Base.setproperty!
abstract type Coords{T, N} <: AbstractArray{T, N} end
abstract type Model end
abstract type Database end
# abstract type Finder end

export *
export Paths, get_properties, getproperty, setproperty!, get_distances, get_momentum, get_kinetics
export Coords, Cartesian, Atomic, Fouriered
export Model, Gaussian
export ImageData

mutable struct Paths
    coords::Coords
    model::Model
    prefix::String
    directory::String
    imgdata::Database
    tag::Dict
    cache::Dict
end

function Paths(;coords="Cartesian", coords_kwargs=Dict(), model="MullerBrown", model_kwargs=Dict(),
       prefix="paths", directory=".", imgdata="ImgData", imgdata_kwargs=Dict(),
       tag=Dict(), cache=Dict())
    coords = getfield(Main, Symbol(coords))(;coords_kwargs...)
    model = getfield(Main, Symbol(model))(;model_kwargs...)
    imgdata = getfield(Main, Symbol(imgdata))(;imgdata_kwargs...)
    Paths(coords, model, prefix, directory, imgdata, tag, cache)
end

function Base.getproperty(paths::Paths, name::Symbol, x)
    if length(String(name)) > 7 && String(name)[end-6:end] == "_kwargs"
        name = Symbol(String(name)[1:end-7])
        field = getfield(paths, name)
        x = x == nothing ? Dict([(f, nothing) for f in fieldnames(field)]) : x
        ans = Dict()
        for (key, value) in pairs(x)
            ans[key] = getproperty(field, key, value)
        end
        return ans
    elseif parentmodule(fieldtype(typeof(paths), name)) ∉ [Base, Core]
        typename = typeof(getfield(paths, name))
        return "$typename"
    else
        return getfield(paths, name)
    end
end

function Base.setproperty!(obj::T, name::Symbol, x) where {T<:Union{Paths, Coords, Database, Model}}
    if length(String(name)) > 7 && String(name)[end-6:end] == "_kwargs"
        name = Symbol(String(name)[1:end-7])
        field = getfield(obj, name)
        for (key, value) in pairs(x)
            setproperty!(field, key, value)
        end
    elseif parentmodule(fieldtype(typeof(obj), name)) ∉ [Base, Core]
        # Change the class
        if typeof(x) == String
            newfield = getfield(Main, Symbol(x))()
            setfield!(obj, name, newfield)
        # Complex update
        elseif typeof(x) <: Dict
            # :coords => {kwargs...} case. Update kwargs.
            if get(x, :name, nothing) == nothing
                field = getfield(obj, name)
                for (key, value) in pairs(x)
                    setproperty!(field, name, x)
                end
            # :coords => {:name=> "Sine", :kwargs => {...}} case. Reconstruct coords
            else
                newname = x[:name]
                new_kwargs = x[:kwargs]
                newfield = getfield(Main, Symbol(newname))(;new_kwargs...)
                setfield!(obj, name, newfield)
            end
        end
    else
        setfield!(obj, name, x)
    end
end

@inline get_distances(paths::Paths, args...; kwargs...) = get_distances(paths.model, paths, args...; kwargs...)
@inline get_momentums(paths::Paths, args...; kwargs...) = get_momentums(paths.model, paths, args...; kwargs...)
@inline get_kinetic_energies(paths::Paths, args...; kwargs...) = get_kinetic_energies(paths.model, paths, args...; kwargs...)
@inline get_kenetic_energy_graident(paths::Paths, args...; kwargs...) = get_kenetic_energy_graident(paths.model, paths, args...; kwargs...)
@inline get_velocities(paths::Paths, args...; kwargs...) = get_velocities(paths.model, paths, args...; kwargs...)
@inline get_accelerations(paths::Paths, args...; kwargs...) = get_accelerations(paths.model, paths, args...; kwargs...)
const get_accelerations = get_acceleration
@inline get_masses(paths::Paths, args...; kwargs...) = get_masses(paths.model, paths, args...; kwargs...)
@inline get_effective_mass(paths::Paths, args...; kwargs...) = get_effective_mass(paths.model, paths, args...; kwargs...)
""" Directly calls the :meth:`get_properties` in ``paths.model``"""
@inline get_properties(paths::Paths, args...; kwargs...) = get_properties(paths.model, paths, args...; kwargs...)
""" Calculate potential """
@inline get_potential(paths::Paths, args...; kwargs...) = get_potential(paths.model, paths, args...; kwargs...)
""" Calculate potential( energy) """
const get_potential_energy = get_potential
""" Calculate potentials, individual energy of each atoms"""
@inline get_potentials(paths::Paths, args...; kwargs...) = get_potentials(paths.model, paths, args...; kwargs...)
""" Equivalanet to Calculate potentials"""
const get_potential_energies = get_potentials
""" Calculate potential gradient(s)"""
@inline get_gradients(paths::Paths, args...; kwargs...) = get_gradients(paths.model, paths, args...; kwargs...)
""" Calculate - potential gradient"""
const get_forces = get_gradients
""" Calculate potential gradient"""
const get_gradient = get_gradients

""" Calculate Hessian of a potential"""
@inline get_hessian(paths::Paths, args...; kwargs...) = get_hessian(paths.model, paths, args...; kwargs...);
""" Calculate kinetic + potential energy"""
function get_total_energy(paths::Paths, args...; kwargs...)
    V = get_potential_energy(paths.model, paths, args...; kwargs...)
    T = get_kinetic_energies(paths.model, paths, args...; kwargs...)
    return V + T
end

""" Calculate covariance. It only applies when potential is guassian"""
@inline get_covariance(paths::Paths, args...; kwargs...) = get_covariance(paths.model, paths, args...; kwargs...)

""" Get index of highest potential energy simplified"""
function get_higest_energy_idx(paths::Paths)
    E = get_potential_energy(path)
    return argmax(E)
end

""" Get index of lowest of covariance simplified"""
function get_lowest_confident_idx(paths::Paths)
    cov = get_covariance(path)
    return argmax(diag(cov))
end

"""" list of int; Get rowid of data"""
@inline get_image_data(paths::Paths, args...; kwargs...) = get_image_data(paths.model::Model, paths, args...; kwargs...)

include("./utils/utils.jl")
include("./coordinates/transformations.jl")
include("./coordinates/coordinates.jl")
include("./database/database.jl")
include("./models/models.jl")

end


export Signal, MLError
export UnboundTypeVar, UnboundVar
export UnusedAnnotation, InvalidSyntax
export DuplicateDeclaration, DuplicateInstanceError
export InstanceNotFound

export RecursiveType
export UnificationFail, NoError
export TypeVarEscape

@data Signal begin
    UnboundTypeVar(Symbol)
    UnboundVar(Symbol)
    UnusedAnnotation(Symbol)
    InvalidSyntax(String)
    DuplicateDeclaration(Symbol)
    DuplicateInstanceError(Vector{InstRec})
    InstanceNotFound(HMT)
    UnsolvedTypeVariables(Set{Var})
    InvalidNamespaceType(HMT)
    UnificationFailOf(HMT, HMT)

    RecursiveType
    UnificationFail
    TypePartialOrderError
    TypeVarEscape
    PrincipalTypeSearchError
    NoError
end

struct MLError <: Exception
    ln :: LineNumberNode  # where the error occurs
    signal :: Signal
end


#== What I want from an End to end function:
3. Save the experimental parameters (as a serializable object?)
4. For each component, save the time and a sparse rep. of the result
5. An easy to plot visualization, separate from these functions
==#

"""
    safety_based_refinement

Performs k-step refinement over uncertain states according to safety verification.
"""
function safety_based_refinement(experiment_params::ExperimentParameters, results_path::String, verification_mat, refinement_steps::Int;
                                 safety_threshold=0.95, horizon=1, reuse_regions_flag=false, reuse_transition_mats_flag=false)

    working_dir = results_path
    refinement_result_dirs = []

    for i=1:refinement_steps
        results_dir = "$results_path/refinement$i"
        states_to_update = findall(x->x<safety_threshold, verification_mat[1:end,3]) ∩ findall(x->x>safety_threshold, verification_mat[1:end,4])
        imdp = refine_imdp(states_to_update, experiment_params, working_dir=working_dir, results_dir=results_dir, reuse_regions_flag=reuse_regions_flag, reuse_transition_mats_flag=reuse_transition_mats_flag)
        verification_mat = Globally(imdp, "safe", horizon, "$results_dir/imdp-r$i.txt")
        save_legacy_mats(verification_mat, results_dir, horizon)
        working_dir = results_dir
        push!(refinement_result_dirs, results_dir)
    end

    return refinement_result_dirs
end

"""
    safety_based_synthesis

Performs synthesis on a system with a focus on remaining in a safe set.
"""
function safety_based_synthesis(experiment_params_array, system_paths::Array{String,1}, results_path::String,
    system_tag::String, label_fcn, refinement_steps::Int;
    minimum_threshold=0.80, horizon=-1, reuse_regions_flag=false, reuse_transition_mats_flag=false)

    exp_dir = results_path

    synth_dir = "$results_path/safety"
    synthesis_mat, results_path, _ = perform_synthesis_from_result_dirs_safety(system_paths, synth_dir, system_tag, label_fcn);
    save_legacy_mats(synthesis_mat, synth_dir, horizon)

    refinement_result_dirs = []

    for i=1:refinement_steps
        results_dir = "$synth_dir/refinement$i"
        states_to_update = findall(x->x<minimum_threshold, synthesis_mat[1:end,3]) ∩ findall(x->x>minimum_threshold, synthesis_mat[1:end,4])

        new_imdp_dirs = Array{String,1}()
        # For each IMDP in the system
        for (j, component_dir) in enumerate(system_paths)
            imdp_results_dir = "$results_dir/mode$j"
            refine_imdp(states_to_update, experiment_params_array[j], working_dir=component_dir, results_dir=imdp_results_dir, reuse_regions_flag=reuse_regions_flag, reuse_transition_mats_flag=reuse_transition_mats_flag)
            push!(new_imdp_dirs, imdp_results_dir)
        end

        cp("$results_dir/mode1/regions.bson", "$results_dir/regions.bson", force=true)

        synthesis_mat, _, _ = perform_synthesis_from_result_dirs_safety(new_imdp_dirs, results_dir, system_tag, label_fcn; horizon=horizon);

        save_legacy_mats(synthesis_mat, results_dir, horizon)

        system_paths = new_imdp_dirs
        push!(refinement_result_dirs, results_dir)
    end

    return refinement_result_dirs
end

"""
Performs k-step refinement over uncertain states according to minimum spec satisfaction from synthesis.
"""
function spec_based_refinement(experiment_params_array, system_paths::Array{String,1}, results_path::String,
                               system_tag::String, label_fcn, refinement_steps::Int;
                               minimum_threshold=0.80, horizon=-1, reuse_regions_flag=false, reuse_transition_mats_flag=false,
                               ltl_formula=nothing)

    refinement_result_dirs = []
    synth_dir = "$results_path/$system_tag/reach-specification"
    !isdir(synth_dir) && mkpath(synth_dir)

    res_mat, res_dir, pimdp = perform_synthesis_from_result_dirs(system_paths, synth_dir, "", experiment_params_array[1].specification_file, label_fcn; add_opt=true, horizon=horizon, ltl_formula=ltl_formula)

    save_legacy_mats(res_mat, synth_dir, horizon)

    for i=1:refinement_steps
        results_dir = "$synth_dir/refinement$i"

        num_dfa_states = Int(length(pimdp.states)/(length(keys(pimdp.imdp_label_dict))))
        states_to_update = unique(Int.(pimdp_col_to_imdp_state.(findall(x->x<minimum_threshold, res_mat[1:end-1,3]) ∩ findall(x->x>minimum_threshold, res_mat[1:end-1,4]), num_dfa_states)))

        new_imdp_dirs = Array{String,1}()
        # For each IMDP in the system
        for (j, component_dir) in enumerate(system_paths)
            imdp_results_dir = "$results_dir/mode$j"
            refine_imdp(states_to_update, experiment_params_array[j], working_dir=component_dir, results_dir=imdp_results_dir, reuse_regions_flag=reuse_regions_flag, reuse_transition_mats_flag=reuse_transition_mats_flag)
            push!(new_imdp_dirs, imdp_results_dir)
        end

        res_mat, res_dir, pimdp = perform_synthesis_from_result_dirs(new_imdp_dirs, results_dir, "", experiment_params_array[1].specification_file, label_fcn; add_opt=true, horizon=horizon, ltl_formula=ltl_formula)
        cp("$results_dir/switched-system/regions.bson", "$results_dir/regions.bson", force=true)
        system_paths = new_imdp_dirs
        push!(refinement_result_dirs, results_dir)
    end

    return refinement_result_dirs
end

function end_to_end_transition_bounds(params; reuse_gps_flag=false, reuse_regions_flag=false)

	@info "Initializing experiment and saving the parameters..."
	exp_dir = create_experiment_directory(params)
	params.experiment_directory = exp_dir
    logfile = initialize_log(exp_dir)
	save_experiment_params(params)
	total_runtime = 0.
    timing_info = Dict()

	regions_filename = "$exp_dir/regions.bson"
	gps_filename = generate_gp_filename(params)

	if reuse_regions_flag && (isfile(regions_filename) && isfile(gps_filename))
		gp_set, gp_info_dict = load_gp_info(gps_filename)
		timing_info["data_generation_time_s"] = -1.
		timing_info["gp_regression_time_s"] = -1.
		region_data = load_region_data("$exp_dir/regions.bson")
		timing_info["data_generation_time_s"] = -1.
		timing_info["gp_regression_time_s"] = -1.
		timing_info["region_bound_time_s"] = -1.
		@goto reuse_regions
	elseif reuse_gps_flag && isfile(gps_filename)
		gp_set, gp_info_dict = load_gp_info(gps_filename)
		timing_info["data_generation_time_s"] = -1.
		timing_info["gp_regression_time_s"] = -1.
		@goto reuse_gps
	end

    @info "Generating the training data..."

    gen_time = @elapsed begin
        x_train, y_train = generate_training_data(params)
    end
    total_runtime += gen_time
    timing_info["data_generation_time_s"] = gen_time
    @info "Data generation time: " gen_time
	save_training_data(x_train, y_train, params.experiment_directory)

    @info "Generating the GP regressions..."
    gp_time = @elapsed begin
        gp_set, gp_info_dict = generate_estimates(params, x_train, y_train)
    end
    total_runtime += gp_time
    timing_info["gp_regression_time_s"] = gp_time
    @info "GP generation time: " gp_time
    save_gp_info(params, gp_set, gp_info_dict)

@label reuse_gps
    @info "Generating the region info..."
    region_time = @elapsed begin
        region_data = generate_region_images(params, gp_info_dict)
    end
    total_runtime += region_time
    timing_info["region_bound_time_s"] = region_time
    @info "Region generation time: " region_time
    save_region_data(exp_dir, region_data)

@label reuse_regions
    @info "Generating the transition bounds..."
    bound_time = @elapsed begin
        result_mats = generate_transition_bounds(params, gp_info_dict, region_data)
    end
    total_runtime += bound_time
    timing_info["transition_bound_time_s"] = bound_time
    @info "Bound generation time: " bound_time
    save_transition_matrices(params, result_mats)

    @info "Total runtime: " total_runtime
    timing_info["total_runtime_s"] = total_runtime
    save_time_info(exp_dir, timing_info)

    flush(logfile)
    close(logfile)

    return timing_info, result_mats
end

function end_to_end_transition_bounds_local_gps(params; single_mode_verification=false, reuse_regions_flag=false)
    exp_dir = create_experiment_directory(params)
	params.experiment_directory = exp_dir
    logfile = initialize_log(params)
    @info "Generating the training data..."
    total_runtime = 0.
    timing_info = Dict()
    gen_time = @elapsed begin
        x_train, y_train = generate_training_data(params)
    end
    total_runtime += gen_time
    timing_info["data_generation_time_s"] = gen_time
    @info "Data generation time: " gen_time
    @info "Generating the regressions and region data..."
    region_time = @elapsed begin
        region_data = generate_region_images(params, x_train, y_train)
    end
    total_runtime += region_time
    timing_info["region_bound_time_s"] = region_time
    @info "Region generation time: " region_time
    save_region_data(params, region_data)
    @info "Generating the transition bounds..."
    bound_time = @elapsed begin
        result_mats = generate_transition_bounds(params, region_data)
    end
    total_runtime += bound_time
    timing_info["transition_bound_time_s"] = bound_time
    @info "Bound generation time: " bound_time
    save_transition_matrices(params, result_mats)

    safety_result_mat = nothing
    if single_mode_verification
        @info "Performing safety verification on single mode for 1 step..."
        verification_time = @elapsed begin
            imdp = create_simple_imdp(result_mats["minPr"], result_mats["maxPr"])
            horizon=1
            verification_result_mat = Globally(imdp, "safe", horizon, "$exp_dir/imdp.txt")
            save_legacy_mats(verification_result_mat, exp_dir, horizon)
        end
        timing_info["single_verification_time_s"] = verification_time
        @info "Verification time: " verification_time
    end

    @info "Total runtime: " total_runtime
    timing_info["total_runtime_s"] = total_runtime
    save_time_info(params, timing_info)

    flush(logfile)
    close(logfile)

    return timing_info, safety_result_mat, result_mats
end

function perform_synthesis_from_result_dirs_safety(res_dirs, exp_dir, system_tag, label_fcn;
	plot_graphs=true, rerun_flag=false, modes=nothing, add_opt=true,
	region_specific="", num_sims=10, sim_points=nothing, horizon=-1)

    @info "Creating the IMDP..."
	imdp = create_imdp_from_result_dirs(res_dirs, "$exp_dir")
    @info("$exp_dir")
	regions_file = @sprintf("%s/regions%s.bson", res_dirs[1], region_specific)
	create_imdp_labels(label_fcn, imdp, regions_file)
	plot_graphs ? create_dot_graph(imdp, "$exp_dir/imdp.gv") : nothing

	# res_mat = run_pimdp_synthesis(pimdp, pimdp_filename, add_opt=add_opt)
    imdp_filename = "$exp_dir/imdp.txt"
    res_mat = CautiousEngine.Globally(imdp, "safe", horizon, imdp_filename, synthesis_flag=true)

    if add_opt
        @info "Synthesizing a controller... (maximize optimistic)"
        res_mat_opt = run_imdp_synthesis(imdp_filename, horizon, mode2="optimistic", save_mats=false)
        for j in 1:length(res_mat[:,1])
            if res_mat[j, 3] == res_mat_opt[j,3] && res_mat_opt[j,4] > res_mat[j,4]
                res_mat[j, 2] = res_mat_opt[j,2]
                res_mat[j, 4] = res_mat_opt[j,4]
            end
        end
    end

    dst_dir = "$exp_dir/safety"
	isdir(dst_dir) ? nothing : mkpath(dst_dir)
	# Copy region data
	sys_dir = res_dirs[1]
	cp("$sys_dir/regions.bson", "$dst_dir/regions.bson", force=true)

	# TODO Plot gamma values somewhere else, i.e. not here
	# if add_opt
	#     plot_gamma_value(dst_dir, res_mat, res_mat_opt, num_dfa_states=length(dfa.states))
	# end
	save_legacy_mats(res_mat, dst_dir, horizon)

	# plot_synthesis_results(dst_dir, res_mat, imdp, dfa, pimdp)
    return res_mat, dst_dir, -1
end

function perform_synthesis_from_result_dirs(res_dirs, exp_dir, system_tag, spec_file, label_fcn;
	plot_graphs=false, rerun_flag=false, modes=nothing, add_opt=false,
	region_specific="", num_sims=10, sim_points=nothing, horizon=-1, ltl_formula=nothing)

	@info "Creating the IMDP..."
	imdp = create_imdp_from_result_dirs(res_dirs, "$exp_dir/$system_tag")
	regions_file = @sprintf("%s/regions%s.bson", res_dirs[1], region_specific)
	create_imdp_labels(label_fcn, imdp, regions_file)
	plot_graphs ? create_dot_graph(imdp, "$exp_dir/$system_tag/imdp.gv") : nothing

	@info "Creating the DFA..."
    if !isnothing(ltl_formula)
        @info "Constructing dfa from ltl formula: $ltl_formula"
        dfa = construct_DFA_from_LTL(ltl_formula)
        spec_tag = string(ltl_formula)
    else
        include("$spec_file")
        spec_tag = basename(spec_file)[1:end-3]
        dfa = DFA(dfa_states, dfa_props, dfa_transitions, dfa_accepting_state, dfa_sink_state, dfa_initial_state)
    end
	dst_dir = "$exp_dir/$system_tag/$spec_tag"
	isdir(dst_dir) ? nothing : mkpath(dst_dir)
	plot_graphs ? create_dot_graph(dfa, "$dst_dir/specification.gv") : nothing

	@info "Creating pimdp..."
	pimdp = construct_DFA_IMDP_product(dfa, imdp)
	plot_graphs ? create_dot_graph(pimdp, "$dst_dir/pimdp.gv") : nothing

	pimdp_filename = "$dst_dir/pimdp.txt"
	res_mat = run_pimdp_synthesis(pimdp, pimdp_filename, add_opt=add_opt, horizon=horizon)

	# Copy region data
	sys_dir = res_dirs[1]
	cp("$sys_dir/regions.bson", "$dst_dir/regions.bson", force=true)

	save_legacy_mats(res_mat, dst_dir, horizon)

	plot_synthesis_results(dst_dir, res_mat, imdp, dfa, pimdp)

	if !isnothing(modes)
		@info "Running a simulation using the resulting controller..."
		trajectories = []
		mt = MersenneTwister(111)

		if !isnothing(sim_points)
			num_sims=length(sim_points)
		end

		for i=1:num_sims
			if !isnothing(sim_points)
				x0 = sim_points[i]
			else
				x0 = 4*rand(mt,2,1)[:]-[2.; 2.]
			end
			extents, _ = deserialize_region_data(regions_file)
			simulate_system(x0, modes, 100, imdp, pimdp, dfa, extents, res_mat[:,2])
			push!(trajectories, copy(pimdp.trajectory))
		end
		plot_synthesis_results(dst_dir, res_mat, imdp, dfa, pimdp, trajectories=trajectories, filename="sim.png")
	end

	return res_mat, dst_dir, pimdp
end


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
struct PV <: AbstractTech
    tilt
    array_type
    module_type
    losses
    azimuth
    name
    location
    existing_kw
    min_kw
    max_kw
    cost_per_kw
    om_cost_per_kw
    degradation_pct
    macrs_option_years
    macrs_bonus_pct
    macrs_itc_reduction
    total_itc_pct
    total_rebate_per_kw
    kw_per_square_foot
    acres_per_kw
    inv_eff
    dc_ac_ratio
    prod_factor_series_kw

    function PV(;
        tilt::Real,
        array_type::Int=1,
        module_type::Int=0,
        losses::Real=0.14,
        azimuth::Real=180,
        name::String="PV",
        location::String="both",
        existing_kw::Real=0,
        min_kw::Real=0,
        max_kw::Real=1.0e5,
        cost_per_kw::Real=1600.0,
        om_cost_per_kw::Real=16.0,
        degradation_pct::Real=0.005,
        macrs_option_years::Int = 5,
        macrs_bonus_pct::Float64 = 1.0,
        macrs_itc_reduction::Float64 = 0.5,
        total_itc_pct::Float64 = 0.26,
        total_rebate_per_kw::Float64 = 0.0,
        kw_per_square_foot::Float64=0.01,
        acres_per_kw::Float64=6e-3,
        inv_eff::Float64=0.96,
        dc_ac_ratio::Float64=1.2,
        prod_factor_series_kw::Union{Missing, Array{Real,1}} = missing
        )

        # validate inputs
        invalid_args = String[]
        if !(0 <= azimuth < 360)
            push!(invalid_args, "azimuth must satisfy 0 <= azimuth < 360, got $(azimuth)")
        end
        if !(array_type in [0, 1, 2, 3, 4])
            push!(invalid_args, "array_type must be in [0, 1, 2, 3, 4], got $(array_type)")
        end
        if !(module_type in [0, 1, 2])
            push!(invalid_args, "module_type must be in [0, 1, 2], got $(module_type)")
        end
        if !(0.0 <= losses <= 0.99)
            push!(invalid_args, "losses must satisfy 0.0 <= losses <= 0.99, got $(losses)")
        end
        if !(0 <= tilt <= 90)
            push!(invalid_args, "tilt must satisfy 0 <= tilt <= 90, got $(tilt)")
        end
        if !(location in ["roof", "ground", "both"])
            push!(invalid_args, "location must be in [\"roof\", \"ground\", \"both\"], got $(location)")
        end
        if !(0.0 <= degradation_pct <= 1.0)
            push!(invalid_args, "degradation_pct must satisfy 0 <= degradation_pct <= 1, got $(degradation_pct)")
        end
        if !(0.0 <= inv_eff <= 1.0)
            push!(invalid_args, "inv_eff must satisfy 0 <= inv_eff <= 1, got $(inv_eff)")
        end
        if !(0.0 <= dc_ac_ratio <= 2.0)
            push!(invalid_args, "dc_ac_ratio must satisfy 0 <= dc_ac_ratio <= 1, got $(dc_ac_ratio)")
        end
        # TODO validate additional args
        if length(invalid_args) > 0
            error("Invalid argument values: $(invalid_args)")
        end

        new(
            tilt,
            array_type,
            module_type,
            losses,
            azimuth,
            name,
            location,
            existing_kw,
            min_kw,
            max_kw,
            cost_per_kw,
            om_cost_per_kw,
            degradation_pct,
            macrs_option_years,
            macrs_bonus_pct,
            macrs_itc_reduction,
            total_itc_pct,
            total_rebate_per_kw,
            kw_per_square_foot,
            acres_per_kw,
            inv_eff,
            dc_ac_ratio,
            prod_factor_series_kw
        )
    end
end


import Dates

import ..AstroDates: Date, DateTime, Time,
    year, month, day,
    hour, minute, second, millisecond,
    fractionofday, yearmonthday, dayofyear

"""
    timescale(ep)

Return the time scale of epoch `ep`.

# Example

```jldoctest
julia> ep = TTEpoch(2000, 1, 1)
2000-01-01T00:00:00.000 TT

julia> timescale(ep)
TT
```
"""
timescale(ep::Epoch) = ep.scale

"""
    DateTime(ep::Epoch)

Convert the epoch `ep` to an `AstroDates.DateTime`.
"""
function DateTime(ep::Epoch)
    if !isfinite(ep.fraction)
        if ep.fraction < 0
            return DateTime(AstroDates.MIN_EPOCH, AstroDates.H00)
        else
            return DateTime(AstroDates.MAX_EPOCH, Time(23, 59, 59.999))
        end
    end

    sec = ep.second + Int64(43200)
    time = sec % Int64(86400)
    if time < 0
        time += Int64(86400)
    end
    date = Int((sec - time) ÷ Int64(86400))

    date_comp = Date(AstroDates.J2000_EPOCH, date)
    time_comp = Time(time, ep.fraction)

    leap = getleap(ep)
    hr = hour(time_comp)
    mn = minute(time_comp)
    leap = ifelse(hr == 23 && mn == 59 && abs(leap) == 1.0, leap, 0.0)
    if !iszero(leap)
        h = hour(time_comp)
        m = minute(time_comp)
        s = second(Float64, time_comp) + leap
        time_comp = Time(h, m, s)
    end

    DateTime(date_comp, time_comp)
end

"""
    year(ep::Epoch)

Get the year of the epoch `ep`.
"""
year(ep::Epoch) = year(DateTime(ep))

"""
    month(ep::Epoch)

Get the month of the epoch `ep`.
"""
month(ep::Epoch) = month(DateTime(ep))

"""
    day(ep::Epoch)

Get the day of the epoch `ep`.
"""
day(ep::Epoch) = day(DateTime(ep))

"""
    yearmonthday(ep::Epoch)

Get the year, month, and day of the epoch `ep` as a tuple.
"""
yearmonthday(ep::Epoch) = yearmonthday(DateTime(ep))

"""
    dayofyear(ep::Epoch)

Get the day of the year of the epoch `ep`.
"""
dayofyear(ep::Epoch) = dayofyear(DateTime(ep))

"""
    hour(ep::Epoch)

Get the hour of the epoch `ep`.
"""
hour(ep::Epoch) = hour(DateTime(ep))

"""
    minute(ep::Epoch)

Get the minute of the epoch `ep`.
"""
minute(ep::Epoch) = minute(DateTime(ep))

"""
    second(type, ep::Epoch)

Get the second of the epoch `ep` as a `type`.
"""
second(typ, ep::Epoch) = second(typ, DateTime(ep))

"""
    second(ep::Epoch) -> Int

Get the second of the epoch `ep` as an `Int`.
"""
second(ep::Epoch) = second(Int, DateTime(ep))

"""
    millisecond(ep::Epoch)

Get the number of milliseconds of the epoch `ep`.
"""
millisecond(ep::Epoch) = millisecond(DateTime(ep))

"""
    Time(ep::Epoch)

Get the `Time` of the epoch `ep`.
"""
Time(ep::Epoch) = Time(DateTime(ep))

"""
    Date(ep::Epoch)

Get the `Date` of the epoch `ep`.
"""
Date(ep::Epoch) = Date(DateTime(ep))

"""
    fractionofday(ep::Epoch)

Get the time of the day of the epoch `ep` as a fraction.
"""
fractionofday(ep::Epoch) = fractionofday(Time(ep))

"""
    Dates.DateTime(ep::Epoch)

Convert the epoch `ep` to a `Dates.DateTime`.
"""
Dates.DateTime(ep::Epoch) = Dates.DateTime(DateTime(ep))

"""
    Dates.Date(ep::Epoch)

Convert the date of epoch `ep` to a `Dates.Date`.
"""
Dates.Date(ep::Epoch) = Dates.Date(Date(ep))

"""
    Dates.Time(ep::Epoch)

Convert the time of epoch `ep` to a `Dates.Time`.
"""
Dates.Time(ep::Epoch) = Dates.Time(Time(ep))


@inline function eatwhitespaces(str::Union{VectorBackedUTF8String, String}, i=1, len=lastindex(str))
    while i<=len
        @inbounds b = codeunit(str, i)

        if b==0x20 # This is ' '
            i += 1
        else
            break
        end
    end
    return i
end

@inline function eatnewlines(str::Union{VectorBackedUTF8String, String}, i=1, len=lastindex(str))
    count = 0
    while i<=len
        @inbounds b = codeunit(str, i)
        if b == 0xd # '\r'
            i += 1
            if i<=len
                @inbounds b = codeunit(str, i)
                if b == 0xa # '\n'
                    i += 1
                end
            end
            count += 1
        elseif b == 0xa
            i += 1
            if i<=len
                @inbounds b = codeunit(str, i)
                if b == 0xd
                    i += 1
                end
            end
            count += 1
        else
            break
        end
    end

    return i, count
end

@inline function tryparsenext_base10_digit(T,str::Union{VectorBackedUTF8String, String},i, len)
    i > len && @goto error
    @inbounds b = codeunit(str,i)
    diff = b-0x30
    diff >= UInt8(10) && @goto error
    return convert(T, diff), i+1

    @label error
    return nothing
end

@inline _isdigit(b::UInt8) = ( (0x30 ≤ b) & (b ≤ 0x39) )

@inline function parse_uint_and_stop(str::Union{VectorBackedUTF8String, String}, i, len, n::T) where {T <: Integer}
    ten = T(10)
    # specialize handling of the first digit so we can return an error
    max_without_overflow = div(typemax(T)-9,10) # the larg
    i <= len || return n, false, i
    @inbounds b = codeunit(str, i)
    diff = b-0x30
    if diff < UInt8(10) && n <= max_without_overflow
        n *= ten
        n += T(diff)
    else
        return n, false, i
    end
    i += 1

    while i <= len && n <= max_without_overflow
        @inbounds b = codeunit(str, i)
        diff = b-0x30
        if diff < UInt8(10)
            n *= ten
            n += T(diff)
        else
            return n, true, i
        end
        i += 1
    end
    return n, true, i
end

@inline function read_digits(str::Union{VectorBackedUTF8String, String}, i, len)
    # slurp up extra digits
    while i <= len
        @inbounds b = codeunit(str, i)
        if !_isdigit(b) # do nothing
            return i
        end
        i += 1
    end
    return i
end

@inline function _is_e(str::Union{VectorBackedUTF8String, String}, i)
    @inbounds b = codeunit(str, i)
    return  (b==0x65) | (b==0x45)
end

@inline function _is_negative(str::Union{VectorBackedUTF8String, String}, i)
    @inbounds b = codeunit(str, i)
    return b==0x2d
end

@inline function _is_positive(str::Union{VectorBackedUTF8String, String}, i)
    @inbounds b = codeunit(str, i)
    return b==0x2b
end

const pre_comp_exp = Float64[10.0^i for i=0:22]

@inline function tryparsenext(::Numeric{F}, str::Union{VectorBackedUTF8String, String}, i, len) where {F<:AbstractFloat}
    R = Nullable{F}

    i>len && @goto error

    negate = false
    @inbounds b = codeunit(str, i)
    if b==0x2d # '-'
        negate = true
        i += 1
    elseif b==0x2b # '+'
        i +=1
    end

    f1::Int64 = 0

    # read an integer up to the decimal point
    f1, rval1, idecpt = parse_uint_and_stop(str, i, len, f1)
    idecpt = read_digits(str, idecpt, len) # get any trailing digits
    i = idecpt

    ie = i
    frac_digits = 0

    # next thing must be dec pt.
    if i <= len && @inbounds(codeunit(str, i)) == 0x2e # Check for '.'
        i += 1
        f1, rval2, ie = parse_uint_and_stop(str, i, len, f1)
        frac_digits = ie - i

        ie = read_digits(str, ie, len) # get any trailing digits
    elseif !rval1 # no first number, and now no deciaml point => invalid
        @goto error
    end

    # Next thing must be exponent
    i = ie
    eval::Int32 = 0

    if i <= len && _is_e(str, i)
        i += 1

        enegate = false
        if i<=len
            if _is_negative(str, i)
                enegate = true
                i += 1
            elseif _is_positive(str, i)
                i += 1
            end
        end
        eval, rval3, i = parse_uint_and_stop(str, i, len, eval)
        if enegate
            eval *= Int32(-1)
        end
    end

    exp = eval - frac_digits

    maxexp = 308
    minexp = -307

    if frac_digits <= 15 && -22 <= exp <= 22
        if exp >= 0
            f = F(f1)*pre_comp_exp[exp+1]
        else
            f = F(f1)/pre_comp_exp[-exp+1]
        end
    else
          f = convert_to_double(f1, exp)
    end

    if negate
        f = -f
    end

    @label done
    return R(convert(F, f)), i

    @label error
    return R(), i
end

function tryparsenext(f::Field{T}, str::Union{VectorBackedUTF8String, String}, i, len, opts::LocalOpts{T_ENDCHAR}) where {T, T_ENDCHAR<:UInt8}
    R = Nullable{T}
    i > len && @goto error
    if f.ignore_init_whitespace
        i = eatwhitespaces(str, i, len)
    end
    @chk2 res, i = tryparsenext(f.inner, str, i, len, opts)

    if f.ignore_end_whitespace
        i0 = i

        while i<=len
            @inbounds b = codeunit(str, i)

            !opts.spacedelim && opts.endchar == 0x09 && b == 0x09 && (i = i+1; @goto done) # 0x09 is \t

            b!=0x20 && b!=0x09 && break
            i=i+1
        end

        opts.spacedelim && i > i0 && @goto done
    end
    # todo don't ignore whitespace AND spacedelim

    if i > len
        if f.eoldelim
            @goto done
        else
            @goto error
        end
    end

    i>len && error("Internal error.")
    @inbounds b = codeunit(str, i)
    opts.spacedelim && (b!=0x20 || b!=0x09) && (i+=1; @goto done)
    !opts.spacedelim && opts.endchar == b && (i+=1; @goto done)

    if f.eoldelim
        if b == 0x0d # '\r'
            i+=1
            if i<=len
                @inbounds b = codeunit(str, i)
                if b == 0x0a # '\n'
                    i+=1
                end
            end
            @goto done
        elseif b == 0x0a # '\n'
            i+=1
            if i<=len
                @inbounds b = codeunit(str, i)
                if b == 0x0d # '\r'
                    i+=1
                end
            end
            @goto done
        end
    end

    @label error
    return R(), i

    @label done
    return R(convert(T, res)), i
end

function tryparsenext(q::Quoted{T,S,<:UInt8,<:UInt8}, str::Union{VectorBackedUTF8String, String}, i, len, opts::LocalOpts{<:UInt8,<:UInt8,<:UInt8}) where {T,S}
    if i>len
        q.required && @goto error
        # check to see if inner thing is ok with an empty field
        @chk2 x, i = tryparsenext(q.inner, str, i, len, opts) error
        @goto done
    end
    @inbounds b = codeunit(str, i)
    ii = i+1
    quotestarted = false
    if q.quotechar == b
        quotestarted = true
        if !q.includequotes
            i = ii
        end

        if q.stripwhitespaces
            i = eatwhitespaces(str, i, len)
        end
    else
        q.required && @goto error
    end

    if quotestarted
        qopts = LocalOpts(q.quotechar, false, q.quotechar, q.escapechar,
                         q.includequotes, q.includenewlines)
        @chk2 x, i = tryparsenext(q.inner, str, i, len, qopts)
    else
        @chk2 x, i = tryparsenext(q.inner, str, i, len, opts)
    end

    if i > len
        if quotestarted && !q.includequotes
            @goto error
        end
        @goto done
    end

    if q.stripwhitespaces
        i = eatwhitespaces(str, i, len)
    end
    i>len && error("Internal error.")
    @inbounds b = codeunit(str, i)
    ii = i + 1

    if quotestarted && !q.includequotes
        b != q.quotechar && @goto error
        i = ii
    end

    @label done
    return Nullable{T}(x), i

    @label error
    return Nullable{T}(), i
end

@inline function isnewline(b::UInt8)
    b == UInt8(10) || b == UInt8(13)
end

function tryparsenext(s::StringToken{T}, str::Union{VectorBackedUTF8String, String}, i, len, opts::LocalOpts{<:UInt8,<:UInt8,<:UInt8}) where {T}
    len = ncodeunits(str)
    inside_quoted_strong = opts.endchar == opts.quotechar
    escapecount = 0
    R = Nullable{T}
    p = UInt8(0)
    i0 = i
    if opts.includequotes
        if i<=len
            @inbounds b = codeunit(str, i)
            if b==opts.quotechar
                # advance counter so that
                # the while loop doesn't react to opening quote
                i += 1
            end
        end
    end

    while i<=len
        @inbounds b = codeunit(str, i)
        ii = i + 1

        if inside_quoted_strong && p==opts.escapechar
            escapecount += 1
        end

        if opts.spacedelim && (b == UInt8(32) || b == UInt8(9)) # 32 = ' ' and 9 = '\t'
            break
        elseif !opts.spacedelim && b == opts.endchar
            if inside_quoted_strong
                # this means we're inside a quoted string
                if opts.quotechar == opts.escapechar
                    # sometimes the quotechar is the escapechar
                    # in that case we need to see the next char
                    if ii > len
                        if opts.includequotes
                            i=ii
                        end
                        break
                    else
                        @inbounds next_b = codeunit(str, ii)
                        if next_b == opts.quotechar
                            # the current character is escaping the
                            # next one
                            i = ii + 1 # skip next char as well
                            p = next_b
                            continue
                        end
                    end
                elseif p == opts.escapechar
                    # previous char escaped this one
                    i = ii
                    p = b
                    continue
                end
            end
            if opts.includequotes
                i = ii
            end
            break
        elseif (!opts.includenewlines && isnewline(b))
            break
        end
        i = ii
        p = b
    end

    return R(_substring(T, str, i0, i-1, escapecount, opts)), i
end

@inline function _substring(::Type{String}, str::Union{VectorBackedUTF8String, String}, i, j, escapecount, opts::LocalOpts{<:UInt8,<:UInt8,<:UInt8})
    if escapecount > 0
        buffer = Vector{UInt8}(undef, j-i+1-escapecount)
        cur_i = i
        cur_buffer_i = 1
        @inbounds c = codeunit(str, cur_i)
        if opts.includequotes && c==opts.quotechar
            @inbounds buffer[cur_buffer_i] = c
            cur_i += 1
            cur_buffer_i += 1
        end
        while cur_i <= j
            @inbounds c = codeunit(str, cur_i)
            if c == opts.escapechar
                next_i = cur_i + 1
                if next_i <= j
                    @inbounds next_c = codeunit(str, next_i)
                    if next_c == opts.quotechar
                        @inbounds buffer[cur_buffer_i] = next_c
                        cur_buffer_i += 1
                        cur_i = next_i
                    end
                else
                    @inbounds buffer[cur_buffer_i] = c
                    cur_buffer_i += 1
                end
            else
                @inbounds buffer[cur_buffer_i] = c
                cur_buffer_i += 1
            end
            cur_i += 1
        end
        return String(buffer)
    else
        return unsafe_string(pointer(str, i), j-i+1)
    end
end


module MKL

const libmkl = Libdl.find_library(["mkl_rt"])
isempty(libmkl) && throw("MKL library cannot be found.")

include("lib/libmkl_dnn.jl")
include("lib/libmkl_dnn_types.jl")

include("convolution.jl")
include("pooling.jl")
include("relu.jl")

function check_dnnerror(e)
    e == E_SUCCESS && return
    e == E_INCORRECT_INPUT_PARAMETER && throw("INCORRECT_INPUT_PARAMETER.")
    e == E_UNEXPECTED_NULL_POINTER && throw("UNEXPECTED_NULL_POINTER.")
    e == E_MEMORY_ERROR && throw("MEMORY_ERROR.")
    e == E_UNSUPPORTED_DIMENSION && throw("UNSUPPORTED_DIMENSION.")
    e == E_UNIMPLEMENTED && throw("UNIMPLEMENTED.")
end

function dnnLayoutCreate{T}(x::Array{T})
    p = Ptr{Cvoid}[0]
    csize = Csize_t[size(x)...]
    cstrides = Csize_t[strides(x)...]
    dnnLayoutCreate_F32(p, ndims(x), csize, cstrides)
    p[1]
end

function dnnPrimitiveAttributesCreate()
    p = Ptr{Cvoid}[0]
    dnnPrimitiveAttributesCreate_F32(p)
    p[1]
end

function dnnExecute(primitive, resources)
    dnnExecute_F32(primitive, resources)
end

function dnnPrimitiveAttributesDestroy(attributes)
    dnnPrimitiveAttributesDestroy_F32(attributes)
end

function dnnDelete(primitive)
    dnnDelete_F32(primitive)
end

end


# Types declarations for Krypto.jl
# Copyright (C): Jaka Smrekar (vinctux) <vinctux@outlook.com>, 2016

# Primitives
abstract CryptoAlgorithm
abstract SymmetricCrypto <: CryptoAlgorithm
abstract AsymmetricCrypto <: CryptoAlgorithm

# Asymmetric
abstract RSA <: AsymmetricCrypto
abstract ECC <: AsymmetricCrypto
abstract RLWE <: AsymmetricCrypto
abstract NTRU <: AsymmetricCrypto

# Symmetric
abstract AES <: SymmetricCrypto
abstract Blowfish <: SymmetricCrypto
abstract Twofish <: SymmetricCrypto
abstract Threefish <: SymmetricCrypto
abstract Serpent <: SymmetricCrypto
abstract Salsa20 <: SymmetricCrypto
abstract RABBIT <: SymmetricCrypto


function make_analog_gui(b::Gtk.GtkBuilder,handles::Tracker_Handles)

    push!(b["analog_c_box"],handles.analog.c)
    setproperty!(handles.analog.c,:hexpand,true)
    setproperty!(handles.analog.c,:vexpand,true)
    show(handles.analog.c)

end

function add_analog_callbacks(b::Gtk.GtkBuilder,handles::Tracker_Handles)

    signal_connect(analog_time_cb,b["analog_x_zoom_minus"],"clicked",Void,(),false,(handles,0))
    signal_connect(analog_time_cb,b["analog_x_zoom_plus"],"clicked",Void,(),false,(handles,1))

    nothing
end

function analog_time_cb(w::Ptr,user_data::Tuple{Tracker_Handles,Int})

    han, time_zoom = user_data

    if time_zoom == 0
        han.analog.t_zoom = han.analog.t_zoom - 1
        if han.analog.t_zoom < 0
            han.analog.t_zoom = 0
        end
    else
        han.analog.t_zoom = han.analog.t_zoom + 1
    end

    update_analog_canvas(han)

    nothing
end

function update_analog_canvas(han::Tracker_Handles)

    frame_range = han.analog.t_zoom
    (lower_id, upper_id) = get_lower_upper_frame(han.displayed_frame,frame_range,han.max_frames)

    ctx=Gtk.getgc(han.analog.c)

    set_source_rgb(ctx,1,1,1)
    paint(ctx)

    w=width(ctx)
    h=height(ctx)

    #center line
    set_source_rgb(ctx,1,0,0)
    cent = (upper_id - han.displayed_frame) / (upper_id - lower_id)
    move_to(ctx,cent * w,0)
    line_to(ctx,cent * w,h)
    stroke(ctx)

    #exclude
    #e_line = make_line(han.man.exclude_block,lower_id,upper_id,w)
    #set_source_rgb(ctx,0,0,0)
    #draw_manual_line(han.analog.c,e_line,2)

    #spikes
    set_source_rgb(ctx,0,0,0)

    #Find points from along signal that are within the frame

    #Get min ind
    a_min=get_min_analog(han,lower_id,1)

    #Get center ind
    get_min_analog(han,han.displayed_frame,1)

    #Get max ind
    a_max=get_max_analog(han,upper_id,1)

    #myline = rand(-100:100,round(Int,w))
    off = h/2

    an_range = range(a_min,a_max,length=round(Int64,w))
    move_to(ctx,0,off+han.analog.var[1][a_min] * han.analog.gains[1])
    aa = a_min + 1
    a_val = 0
    ww = 1
    while aa < a_max

        #=
        if abs(han.analog.var[1][aa]) > a_val
            a_val = han.analog.var[1][aa]
        end

        if aa > an_range[ww]
            line_to(ctx,ww,a_val * han.analog.gains[1] + off)
            a_val = 0
            ww += 1
        end
        =#
        line_to(ctx,ww + aa / an_range[ww],han.analog.var[1][aa] * han.analog.gains[1] + off)

        if aa > an_range[ww]
            ww += 1
        end
        aa += 1
    end
    stroke(ctx)

    reveal(han.analog.c)

    nothing
end

function get_min_analog(han,b1::Int,an::Int)

    findfirst(han.analog.ts[an].>b1)[1]
end

function get_max_analog(han,b1::Int,an::Int)

    findfirst(han.analog.ts[an].>b1)[1]-1
end


function trophicspecies(N::T; clustering_function::Function=EAJS) where {T <: UnipartiteNetwork}

    similarities = clustering_function(N)

    # TODO make a matrix based on overlap

end