module TweetNaCl

import BinDeps

type SignatureFailure <: Exception end

#@BinDeps.load_dependencies
if( isfile(joinpath(dirname(@__FILE__), "..", "deps", "deps.jl")) )
    include("../deps/deps.jl")
else
    error("TweetNaCl not properly built.  Please run Pkg.build(\"TweetNaCl\").")
end

nacl_sign_public_key_length = 0;
nacl_sign_secret_key_length = 0;
nacl_sign_max_extra_length  = 0;

function __init__()
    global nacl_sign_public_key_length,
           nacl_sign_secret_key_length,
           nacl_sign_max_extra_length

    retval = ccall( (:init_randombytes, tweetnacl), Cint, () )
    if retval != 0
        error("Failed to initialise cryptographic PRNG.")
    end

    nacl_sign_public_key_length = ccall(
        (:nacl_get_sign_pk_length, tweetnacl), Cint, () )

    nacl_sign_secret_key_length = ccall(
        (:nacl_get_sign_sk_length, tweetnacl), Cint, () )

    nacl_sign_max_extra_length = ccall(
        (:nacl_get_sign_sig_extra_length, tweetnacl), Cint, () )

end

function randombytes!(str::Array{UInt8})
    ccall( (:randombytes, TweetNaCl.tweetnacl), Void, (Ptr{UInt8}, Culonglong), str, length(str))
end

function randombytes(count::Int64)
    str = Array(UInt8, count)
    randombytes!(str)
    return str
end

function crypto_sign_keypair()
    public_key = Array(UInt8, nacl_sign_public_key_length)
    secret_key = Array(UInt8, nacl_sign_secret_key_length)

    ccall( (:crypto_sign_ed25519_tweet_keypair, TweetNaCl.tweetnacl), Cint,
           (Ptr{UInt8}, Ptr{UInt8}), public_key, secret_key)

    return public_key, secret_key
end

function crypto_sign(message::AbstractString, secret_key)

    if(length(secret_key) != TweetNaCl.nacl_sign_secret_key_length)
        error("crypto_sign: Secret key is of incorrect length.")
    end

    signature = zeros(UInt8, length(message)+nacl_sign_max_extra_length)
    signature_length = zeros(UInt64, 1)

    ccall( (:crypto_sign_ed25519_tweet, TweetNaCl.tweetnacl), Cint,
           (Ptr{UInt8}, Ptr{Culonglong}, Ptr{UInt8}, Culonglong, Ptr{UInt8}),
           signature, signature_length, message, length(message), secret_key)

    return signature[1:signature_length[1]]
end

function crypto_sign_open(signature::Array{UInt8}, public_key)

    if(length(public_key) != TweetNaCl.nacl_sign_public_key_length)
        error("crypto_sign_open: Public key is of incorrect length.")
    end

    message = zeros(UInt8, length(signature))
    message_length = zeros(UInt64, 1)

    result = ccall( (:crypto_sign_ed25519_tweet_open, TweetNaCl.tweetnacl), Cint,
           (Ptr{UInt8}, Ptr{Culonglong}, Ptr{UInt8}, Culonglong, Ptr{UInt8}),
           message, message_length, signature, length(signature), public_key)

    if result != 0
        throw(SignatureFailure())
    end

    return message[1:message_length[1]]

end

end # module


# *********************************************************************************
# 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 MPCInputs <: AbstractInputs
    techs::Array{String, 1}
    pvtechs::Array{String, 1}
    gentechs::Array{String,1}
    elec_techs::Array{String, 1}
    techs_no_turndown::Array{String, 1}
    existing_sizes::DenseAxisArray{Float64, 1}  # (techs)
    elec_load::MPCElectricLoad
    time_steps::UnitRange
    time_steps_with_grid::Array{Int, 1}
    time_steps_without_grid::Array{Int, 1}
    hours_per_timestep::Float64
    months::UnitRange
    production_factor::DenseAxisArray{Float64, 2}  # (techs, time_steps)
    levelization_factor::DenseAxisArray{Float64, 1}  # (techs)
    VoLL::Array{R, 1} where R<:Real #default set to 1 US dollar per kwh
    pwf_e::Float64
    pwf_om::Float64
    two_party_factor::Float64
    etariff::MPCElectricTariff
    ratchets::UnitRange
    techs_by_exportbin::DenseAxisArray{Array{String,1}}  # indexed on [:NEM, :WHL]
    storage::MPCStorage
    generator::MPCGenerator
    elecutil::ElectricUtility
    max_grid_export_kwh::Float64
    export_bins_by_tech::Dict{String, Array{Symbol, 1}}
end

function MPCInputs(fp::String)
    s = MPCScenario(JSON.parsefile(fp))
    MPCInputs(s)
end

function MPCInputs(s::MPCScenario)

    time_steps = 1:length(s.electric_load.loads_kw)
    hours_per_timestep = 1 / s.settings.time_steps_per_hour
    techs, pvtechs, gentechs, production_factor, existing_sizes = setup_tech_inputs(s)
    elec_techs = techs  # only modeling electric loads/techs so far
    techs_no_turndown = pvtechs
    max_grid_export_kwh = sum(s.electric_load.loads_kw)
    months = 1:length(s.electric_tariff.monthly_demand_rates)

    techs_by_exportbin = DenseAxisArray([ techs, techs, techs], s.electric_tariff.export_bins)
    # TODO account for which techs have access to export bins (when we add more techs than PV)

    levelization_factor = DenseAxisArray(repeat([1.0], length(techs)), techs)
    pwf_e = 1.0
    pwf_om = 1.0
    two_party_factor = 1.0

    time_steps_with_grid, time_steps_without_grid, = setup_electric_utility_inputs(s)

    export_bins_by_tech = Dict{String, Array{Symbol, 1}}()
    for t in elec_techs
        export_bins_by_tech[t] = s.electric_tariff.export_bins
    end
    # TODO implement export bins by tech (rather than assuming that all techs share the export_bins)

    MPCInputs(
        techs,
        pvtechs,
        gentechs,
        elec_techs,
        techs_no_turndown,
        existing_sizes,
        s.electric_load,
        time_steps,
        time_steps_with_grid,
        time_steps_without_grid,
        hours_per_timestep,
        months,
        production_factor,
        levelization_factor,  # TODO need this?
        typeof(s.financial.VoLL) <: Array{<:Real, 1} ? s.financial.VoLL : fill(s.financial.VoLL, length(time_steps)),
        pwf_e,
        pwf_om,
        two_party_factor,
        # maxsize_pv_locations,
        s.electric_tariff,
        1:length(s.electric_tariff.tou_demand_ratchet_timesteps),  # ratchets
        techs_by_exportbin,
        s.storage,
        s.generator,
        s.electric_utility,
        max_grid_export_kwh,
        export_bins_by_tech
        # s.site.min_resil_timesteps,
        # s.site.mg_tech_sizes_equal_grid_sizes,
        # s.site.node
    )
end

function setup_tech_inputs(s::MPCScenario)

    pvtechs = String[pv.name for pv in s.pvs]
    if length(Base.Set(pvtechs)) != length(pvtechs)
        error("PV names must be unique, got $(pvtechs)")
    end

    techs = copy(pvtechs)
    gentechs = String[]
    if s.generator.size_kw > 0
        push!(techs, "Generator")
        push!(gentechs, "Generator")
    end

    time_steps = 1:length(s.electric_load.loads_kw)

    # REoptInputs indexed on techs:
    existing_sizes = DenseAxisArray{Float64}(undef, techs)
    production_factor = DenseAxisArray{Float64}(undef, techs, time_steps)

    if !isempty(pvtechs)
        setup_pv_inputs(s, existing_sizes, production_factor)
    end

    if "Generator" in techs
        setup_gen_inputs(s, existing_sizes, production_factor)
    end

    return techs, pvtechs, gentechs, production_factor, existing_sizes
end

function setup_pv_inputs(s::MPCScenario, existing_sizes, production_factor)
    for pv in s.pvs
        production_factor[pv.name, :] = pv.prod_factor_series_kw
        existing_sizes[pv.name] = pv.size_kw
    end
    return nothing
end

function setup_gen_inputs(s::MPCScenario, existing_sizes, production_factor)
    existing_sizes["Generator"] = s.generator.size_kw
    production_factor["Generator", :] = ones(length(s.electric_load.loads_kw))
    return nothing
end


# libcublas_types.jl
#
# Initially generated with wrap_c from Clang.jl. Modified to remove anonymous
# enums and add cublasContext.
#
# Author: Nick Henderson <nwh@stanford.edu>
# Created: 2014-08-27
# License: MIT
#

# begin enum cublasStatus_t
const cublasStatus_t = UInt32
const CUBLAS_STATUS_SUCCESS = 0
const CUBLAS_STATUS_NOT_INITIALIZED = 1
const CUBLAS_STATUS_ALLOC_FAILED = 3
const CUBLAS_STATUS_INVALID_VALUE = 7
const CUBLAS_STATUS_ARCH_MISMATCH = 8
const CUBLAS_STATUS_MAPPING_ERROR = 11
const CUBLAS_STATUS_EXECUTION_FAILED = 13
const CUBLAS_STATUS_INTERNAL_ERROR = 14
const CUBLAS_STATUS_NOT_SUPPORTED = 15
const CUBLAS_STATUS_LICENSE_ERROR = 16
# end enum cublasStatus_t
# begin enum cublasFillMode_t
const cublasFillMode_t = UInt32
const CUBLAS_FILL_MODE_LOWER = 0
const CUBLAS_FILL_MODE_UPPER = 1
# end enum cublasFillMode_t
# begin enum cublasDiagType_t
const cublasDiagType_t = UInt32
const CUBLAS_DIAG_NON_UNIT = 0
const CUBLAS_DIAG_UNIT = 1
# end enum cublasDiagType_t
# begin enum cublasSideMode_t
const cublasSideMode_t = UInt32
const CUBLAS_SIDE_LEFT = 0
const CUBLAS_SIDE_RIGHT = 1
# end enum cublasSideMode_t
# begin enum cublasOperation_t
const cublasOperation_t = UInt32
const CUBLAS_OP_N = 0
const CUBLAS_OP_T = 1
const CUBLAS_OP_C = 2
# end enum cublasOperation_t
# begin enum cublasPointerMode_t
const cublasPointerMode_t = UInt32
const CUBLAS_POINTER_MODE_HOST = 0
const CUBLAS_POINTER_MODE_DEVICE = 1
# end enum cublasPointerMode_t
# begin enum cublasAtomicsMode_t
const cublasAtomicsMode_t = UInt32
const CUBLAS_ATOMICS_NOT_ALLOWED = 0
const CUBLAS_ATOMICS_ALLOWED = 1
# end enum cublasAtomicsMode_t
const cublasContext = Nothing
const cublasHandle_t = Ptr{cublasContext}
# complex numbers in cuda
const cuComplex = Complex{Float32}
const cuDoubleComplex = Complex{Float64}
# complex types from Base/linalg.jl
const CublasFloat = Union{Float64,Float32,ComplexF64,ComplexF32}
const CublasReal = Union{Float64,Float32}
const CublasComplex = Union{ComplexF64,ComplexF32}
# FP16 (cuda_fp16.h) in cuda
const __half = Float16
struct __half2
    x1::__half
    x2::__half
end

if CUDAdrv.version() >= v"0.7.5"
    # specify which GEMM algorithm to use in cublasGemmEx() (CUDA 7.5+)
    const cublasGemmAlgo_t = Int32
    const CUBLAS_GEMM_DFALT = -1
    const CUBLAS_GEMM_ALGO0 = 0
    const CUBLAS_GEMM_ALGO1 = 1
    const CUBLAS_GEMM_ALGO2 = 2
    const CUBLAS_GEMM_ALGO3 = 3
    const CUBLAS_GEMM_ALGO4 = 4
    const CUBLAS_GEMM_ALGO5 = 5
    const CUBLAS_GEMM_ALGO6 = 6
    const CUBLAS_GEMM_ALGO7 = 7
    # specify which DataType to use with cublas<t>gemmEx() and cublasGemmEx() (CUDA 7.5+) functions
    const cudaDataType_t = UInt32
    const CUDA_R_16F = UInt32(2)
    const CUDA_C_16F = UInt32(6)
    const CUDA_R_32F = UInt32(0)
    const CUDA_C_32F = UInt32(4)
    const CUDA_R_64F = UInt32(1)
    const CUDA_C_64F = UInt32(5)
    const CUDA_R_8I  = UInt32(3)
    const CUDA_C_8I  = UInt32(7)
    const CUDA_R_8U  = UInt32(8)
    const CUDA_C_8U  = UInt32(9)
    const CUDA_R_32I = UInt32(10)
    const CUDA_C_32I = UInt32(11)
    const CUDA_R_32U = UInt32(12)
    const CUDA_C_32U = UInt32(13)
end

@enum CUBLASMathMode::Cint begin
   CUBLAS_DEFAULT_MATH = 0
   CUBLAS_TENSOR_OP_MATH = 1
end


function backtest(strat, data; mode = "train")
    记忆仓位 = @staticvar zeros(Float32, 100000)
    F, N, T = size(data.特征)
    T < 1 && return 0f0
    @unpack sim, 最大持仓, 持仓天数, 最多交易次数, 禁止平今,
        夜盘最早开仓时间, 夜盘最晚开仓时间, 夜盘最晚平仓时间,
        早盘最早开仓时间, 早盘最晚开仓时间,早盘最晚平仓时间 = strat
    最大持仓 = iszero(最大持仓) ? ncodes(data) : clamp(最大持仓, 1, ncodes(data))
    虚拟信号, 综合评分 = simulate(sim, data)
    持仓天数′ = size(虚拟信号, 1) ÷ N
    if 持仓天数′ == 1
        虚拟信号 = lockphase(虚拟信号, 持仓天数)
        综合评分 = lockphase(综合评分, 持仓天数)
    else
        持仓天数 = 持仓天数′
    end
    data = repeat(data, 持仓天数)
    @unpack 涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 涨停, 跌停, 交易池 = data
    map!(fillnan, 时间戳, 时间戳)
    if size(记忆仓位, 1) != size(虚拟信号, 1)
        fill!(resize!(记忆仓位, size(虚拟信号, 1)), 0f0)
    end
    转移函数 = transition(sim)
    # TODO: merge select
    if select(sim)
        是否为ST = repeat(getfeat(data, r"is_st", errors = "ignore"), 持仓天数)
        实际仓位 = select_stocks(时间戳, 代码, 买1价, 卖1价, 涨停, 跌停, 交易池, 是否为ST, 虚拟信号, 综合评分,
                    记忆仓位, 最大持仓, 持仓天数, 最多交易次数, 转移函数, 禁止平今, 夜盘最早开仓时间, 夜盘最晚开仓时间,
                    夜盘最晚平仓时间, 早盘最早开仓时间, 早盘最晚开仓时间, 早盘最晚平仓时间)
    else
        实际仓位 = constraint(时间戳, 代码, 买1价, 卖1价, 涨停, 跌停, 虚拟信号, 记忆仓位, 转移函数, 最多交易次数, 禁止平今,
                    夜盘最早开仓时间, 夜盘最晚开仓时间, 夜盘最晚平仓时间, 早盘最早开仓时间, 早盘最晚开仓时间, 早盘最晚平仓时间)
    end
    if size(实际仓位, 2) == 1 && !any(isnan, 实际仓位)
        记忆仓位 .= 实际仓位[:, end]
    end
    if T > 1 && parseenv("CLOSE_LAST_POS", true)
        实际仓位[:, end] .= 0
    end
    pnl = if mode == "train"
        summarize_train(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 最大持仓)
    else
        summarize_test(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 虚拟信号, 最大持仓 * 持仓天数)
    end
    return pnl
end

function constraint(时间戳, 代码, 买1价, 卖1价, 涨停, 跌停, 虚拟信号, 记忆仓位, 转移函数, 最多交易次数, 禁止平今, 夜盘最早开仓时间, 夜盘最晚开仓时间, 夜盘最晚平仓时间, 早盘最早开仓时间, 早盘最晚开仓时间, 早盘最晚平仓时间)
    实际仓位 = zero(虚拟信号)
    N, T = size(实际仓位)
    交易次数 = zeros(Float32, N)
    @inbounds for t in 1:T, n in 1:N
        小时 = to_trade_hour(时间戳[n, t])
        之前仓位 = t > 1 ? 实际仓位[n, t - 1] : 记忆仓位[n]
        是否平仓 = 夜盘最晚平仓时间 <= 小时 <= 8 || 早盘最晚平仓时间 <= 小时 <= 16
        当前仓位 = 是否平仓 ? 0f0 : 转移函数(之前仓位, 虚拟信号[n, t])
        if (涨停[n, t] == 1 || isnan(卖1价[n, t])) && 当前仓位 > 之前仓位 ||
            (跌停[n, t] == 1 || isnan(买1价[n, t])) && 当前仓位 < 之前仓位 ||
            abs(当前仓位) > abs(之前仓位) && (交易次数[n] >= 最多交易次数 ||
                !(夜盘最早开仓时间 <= 小时 <= 夜盘最晚开仓时间) ||
                !(早盘最早开仓时间 <= 小时 <= 早盘最晚开仓时间)) ||
            abs(当前仓位) < abs(之前仓位) && 交易次数[n] >= 1 && 禁止平今 && !是否平仓
            当前仓位 = 之前仓位
        end
        时间差 = 时间戳[n, t] - 时间戳[n, max(1, t - 1)]
        if 小时 > 8 && 时间差 > 3600 * 5
            交易次数[n] = 0
        else
            交易次数[n] += abs(当前仓位 - 之前仓位)
        end
        if 代码[n, t] != 代码[n, min(t + 1, end)] # || 时间戳[n, t] == 0
            当前仓位 = 0f0
            交易次数[n] = 0
        end
        实际仓位[n, t] = 当前仓位
    end
    return 实际仓位
end

function select_stocks(时间戳, 代码, 买1价, 卖1价, 涨停, 跌停, 交易池, 是否为ST, 虚拟信号, 综合评分,
                记忆仓位, 最大持仓, 持仓天数, 最多交易次数, 转移函数, 禁止平今, 夜盘最早开仓时间,
                夜盘最晚开仓时间, 夜盘最晚平仓时间, 早盘最早开仓时间, 早盘最晚开仓时间, 早盘最晚平仓时间)
    实际仓位 = zero(虚拟信号)
    N, T = size(虚拟信号)
    交易次数 = zeros(Int, N)
    选股池 = zeros(Bool, N)
    for t in 1:T
        fill!(选股池, 0)
        选股数 = 最大持仓 * 持仓天数
        for n in 1:N
            小时 = to_trade_hour(时间戳[n, t])
            之前仓位 = t > 1 ? 实际仓位[n, t - 1] : 记忆仓位[n]
            是否平仓 = 夜盘最晚平仓时间 <= 小时 <= 8 || 早盘最晚平仓时间 <= 小时 <= 16
            当前仓位 = 是否平仓 ? 0f0 : 转移函数(之前仓位, 虚拟信号[n, t])
            if (涨停[n, t] == 1 || isnan(卖1价[n, t])) && 当前仓位 > 之前仓位 ||
                (跌停[n, t] == 1 || isnan(买1价[n, t])) && 当前仓位 < 之前仓位 ||
                abs(当前仓位) > abs(之前仓位) && (交易次数[n] >= 最多交易次数 ||
                    !(夜盘最早开仓时间 <= 小时 <= 夜盘最晚开仓时间) ||
                    !(早盘最早开仓时间 <= 小时 <= 早盘最晚开仓时间)) ||
                abs(当前仓位) < abs(之前仓位) && 交易次数[n] >= 1 && 禁止平今 && !是否平仓
                当前仓位 = 之前仓位
            end
            时间差 = 时间戳[n, t] - 时间戳[n, max(1, t - 1)]
            if 小时 > 8 && 时间差 > 3600 * 5
                交易次数[n] = 0
            else
                交易次数[n] += 当前仓位 != 之前仓位
            end
            if 代码[n, t] != 代码[n, min(t + 1, end)] || 时间戳[n, t] == 0
                当前仓位 = 0f0
                交易次数[n] = 0
            end
            if isnan(虚拟信号[n, t]) || 当前仓位 > 之前仓位
                选股池[n] = 1
                实际仓位[n, t] = 0
            else
                实际仓位[n, t] = 当前仓位
            end
            if 代码[n, t] != 代码[n, min(t + 1, end)] || 时间戳[n, t] == 0
                实际仓位[n, t] = 0
                选股池[n] = 0
                交易次数[n] = 0
            elseif 交易池[n, t] == 0 || 是否为ST[n, t] == 1
                实际仓位[n, t] = 0
                选股池[n] = 0
            elseif 涨停[n, t] == 1 || 跌停[n, t] == 1
                实际仓位[n, t] = 之前仓位
                选股池[n] = 0
            end
            选股数 -= 实际仓位[n, t] == 1
        end
        选股数 = min(选股数, 最大持仓)
        选股列表 = findall(选股池)
        if length(选股列表) <= 选股数
            实际仓位[选股列表, t] .= 1
        else
            选股列表索引 = partialsortperm(综合评分[选股列表, t], 1:选股数, rev = true)
            实际仓位[选股列表[选股列表索引], t] .= 1
        end
    end
    return 实际仓位
end

function summarize_core(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 最大持仓)
    记忆收益率 = @staticvar Dict{UInt64, Array{Float32}}()
    N, T = size(实际仓位)
    天数 = 1
    for n in 1:size(时间戳, 1)
        时间戳一行 = filter(!iszero, 时间戳[n, :])
        if !isempty(时间戳一行)
            天数 = sortednunique(unix2date, 时间戳一行)
            break
        end
    end
    复利 = get(ENV, "USE_COMPLEX", "0") == "1"
    收益率 = zero(实际仓位)
    之前仓位 = copy(记忆仓位)
    之前盈亏 = zeros(Float32, N)
    @inbounds for t in 1:T
        当前平均涨幅 = @views mean(涨幅[:, t])
        for n in 1:N
            当前仓位 = 实际仓位[n, t]
            仓位变化 = 当前仓位 - 之前仓位[n]
            买滑点 = fillnan((卖1价[n, t] - 最新价[n, t]) ⧶ 最新价[n, t])
            卖滑点 = fillnan((最新价[n, t] - 买1价[n, t]) ⧶ 最新价[n, t])
            交易成本 = fillnan(手续费率[n, t]) * abs(仓位变化)
            滑点 = ifelse(仓位变化 > 0, 买滑点, -卖滑点) * 仓位变化
            收益率[n, t] = 之前盈亏[n] - 交易成本 - 滑点
            之前仓位[n] = 当前仓位
        end
        t < T && for n in 1:N
            当前仓位 = 实际仓位[n, t]
            未来涨幅 = fillnan(涨幅[n, t + 1])
            之前盈亏[n] = 当前仓位 * 未来涨幅
        end
    end
    倍数 = 年化收益率 = 1f0
    资金曲线 = ones(Float32, T)
    if 复利
        @inbounds for t in 1:size(实际仓位, 2)
            Δ = @views sum(收益率[:, t]) ⧶ 最大持仓
            资金曲线[t] = 倍数 = (1 + Δ)  * 倍数
        end
        年化收益率 = 倍数^(240f0 / 天数) - 1
    else
        @inbounds for t in 1:T
            Δ = @views sum(收益率[:, t]) ⧶ 最大持仓
            资金曲线[t] = 倍数 = 倍数 + Δ
        end
        年化收益率 = 240f0 * (倍数 - 1f0) / 天数
    end
    最大回撤, 最大回撤期 = drawdown(资金曲线)
    夏普率 = sharperatio(资金曲线, 240T / 天数)
    length(记忆收益率) > 10 && empty!(记忆收益率)
    记忆收益率[objectid(时间戳)] = 收益率
    return 收益率, 资金曲线, 年化收益率, 最大回撤, 夏普率
end

function summarize_train(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 最大持仓)
    收益率, 资金曲线, 年化收益率, 最大回撤, 夏普率 =
        summarize_core(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 最大持仓)
    评分模式 = parseenv("SCORE", "R")
    if 评分模式 == "R" # Return
        分数 = 年化收益率
    elseif 评分模式 == "SHARPE"
        分数 = 夏普率
    elseif 评分模式 == "RoMaD" # Return Over Maximum Drawdown
        分数 = 年化收益率 ⧶ abs(最大回撤)
    elseif occursin("LAR", 评分模式)  # Loss Amplified Return
        λ = Meta.parse(split(评分模式, '_')[2])
        分数 = sum(x -> amploss(x, λ), 年化收益率)
    elseif occursin("IR", 评分模式)  # Information Rank
        nonan(x) = ifelse(isnan(x), zero(x), x)
        分数 = mean(nonan(corspearman(综合评分[:, t], 涨幅[:, t])) for t in 1:size(实际仓位, 2))
    elseif occursin("WR", 评分模式)  # Average Winning Ratio
        分数 = mean(mean(涨幅[实际仓位[:, t] .> 0, t]) >
                    mean(涨幅[交易池[:, t] .> 0, t])
                    for t in 1:(size(实际仓位, 2) - 1))
    elseif occursin("ACC", 评分模式) # Accuracy
        分数 = mean(mean(涨幅[实际仓位[:, t] .> 0, t] .>
                        mean(涨幅[交易池[:, t] .> 0, t]))
                    for t in 1:(size(实际仓位, 2) - 1))
    end
    @assert !isnan(分数)
    return 分数
end

function summarize_test(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 虚拟信号, 最大持仓)
    收益率, 资金曲线, 年化收益率, 最大回撤, 夏普率 = summarize_core(涨幅, 时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 交易池, 记忆仓位, 实际仓位, 综合评分, 最大持仓)
    目录名 = mkpath(产生目录(时间戳, 代码, 年化收益率, 最大回撤, 夏普率))
    @indir 目录名 begin
        输出每日持股明细(代码, 时间戳, 实际仓位)
        输出资金和仓位曲线(时间戳, 实际仓位, 资金曲线)
        输出个股交易记录(时间戳, 代码, 最新价, 买1价, 卖1价, 手续费率, 收益率, 实际仓位)
        新目录名 = 输出资金曲线(时间戳, 代码, 实际仓位, 收益率, 最大持仓)
        报告后处理()
        parseenv("OUTPUT_POS", false) && 输出仓位评分信号(代码, 时间戳, 实际仓位, 综合评分, 虚拟信号)
        parseenv("OUTPUT_MINPOS", false) && 输出分钟仓位(代码, 时间戳, 最新价, 实际仓位, 收益率)
    end
    return 年化收益率
end


module Inference

using ..Core, ..Core.RandomVariables, ..Utilities
using ..Core.RandomVariables: Metadata, _tail, TypedVarInfo,
    islinked, invlink!, getlogp, tonamedtuple
using Distributions, Libtask, Bijectors
using ProgressMeter, LinearAlgebra
using ..Turing: PROGRESS, CACHERESET, AbstractSampler
using ..Turing: Model, runmodel!, get_pvars, get_dvars,
    Sampler, SampleFromPrior, SampleFromUniform,
    Selector, AbstractSamplerState
using ..Turing: in_pvars, in_dvars, Turing
using StatsFuns: logsumexp
using Random: GLOBAL_RNG, AbstractRNG
using ..Turing.Interface

import MCMCChains: Chains
import AdvancedHMC; const AHMC = AdvancedHMC
import ..Turing: getspace
import Distributions: sample
import ..Core: getchunksize, getADtype
import ..Interface: AbstractTransition, sample, step!, sample_init!,
    transitions_init, sample_end!, AbstractSampler, transition_type,
    callback, init_callback, AbstractCallback, psample

export  InferenceAlgorithm,
        Hamiltonian,
        AbstractGibbs,
        GibbsComponent,
        StaticHamiltonian,
        AdaptiveHamiltonian,
        SampleFromUniform,
        SampleFromPrior,
        MH,
        Gibbs,      # classic sampling
        HMC,
        SGLD,
        SGHMC,
        HMCDA,
        NUTS,       # Hamiltonian-like sampling
        DynamicNUTS,
        IS,
        SMC,
        CSMC,
        PG,
        PIMH,
        PMMH,
        IPMCMC,  # particle-based sampling
        assume,
        observe,
        step,
        WelfordVar,
        WelfordCovar,
        NaiveCovar,
        get_var,
        get_covar,
        add_sample!,
        reset!,
        step!

#######################
# Sampler abstraction #
#######################
abstract type AbstractAdapter end
abstract type InferenceAlgorithm end
abstract type ParticleInference <: InferenceAlgorithm end
abstract type Hamiltonian{AD} <: InferenceAlgorithm end
abstract type StaticHamiltonian{AD} <: Hamiltonian{AD} end
abstract type AdaptiveHamiltonian{AD} <: Hamiltonian{AD} end

getchunksize(::T) where {T <: Hamiltonian} = getchunksize(T)
getchunksize(::Type{<:Hamiltonian{AD}}) where AD = getchunksize(AD)
getADtype(alg::Hamiltonian) = getADtype(typeof(alg))
getADtype(::Type{<:Hamiltonian{AD}}) where {AD} = AD

"""
    mh_accept(H::T, H_new::T, log_proposal_ratio::T) where {T<:Real}

Peform MH accept criteria with log acceptance ratio. Returns a `Bool` for acceptance.

Note: This function is only used in PMMH.
"""
function mh_accept(H::T, H_new::T, log_proposal_ratio::T) where {T<:Real}
    return log(rand()) + H_new < H + log_proposal_ratio, min(0, -(H_new - H))
end

######################
# Default Transition #
######################

struct Transition{T, F<:AbstractFloat} <: AbstractTransition
    θ  :: T
    lp :: F
end

function Transition(spl::Sampler, nt::NamedTuple=NamedTuple())
    theta = merge(tonamedtuple(spl.state.vi), nt)
    lp = getlogp(spl.state.vi)
    return Transition{typeof(theta), typeof(lp)}(theta, lp)
end

function additional_parameters(::Type{<:Transition})
    return [:lp]
end

Interface.transition_type(::Sampler{alg}) where alg = transition_type(alg)

##########################################
# Internal variable names for MCMCChains #
##########################################

const TURING_INTERNAL_VARS = (internals = [
    "elapsed",
    "eval_num",
    "lf_eps",
    "lp",
    "weight",
    "le",
    "acceptance_rate",
    "hamiltonian_energy",
    "hamiltonian_energy_error",
    "max_hamiltonian_energy_error",
    "is_accept",
    "log_density",
    "n_steps",
    "numerical_error",
    "step_size",
    "nom_step_size",
    "tree_depth",
    "is_adapt",
],)

#########################################
# Default definitions for the interface #
#########################################

function Interface.sample(
    rng::AbstractRNG,
    model::ModelType,
    alg::AlgType,
    N::Integer;
    kwargs...
) where {
    ModelType<:Sampleable,
    SamplerType<:AbstractSampler,
    AlgType<:InferenceAlgorithm
}
    return sample(rng, model, Sampler(alg, model), N; progress=PROGRESS[], kwargs...)
end

function Interface.sample(
    model::ModelType,
    alg::AlgType,
    N::Integer;
    resume_from=nothing,
    kwargs...
) where {
    ModelType<:Sampleable,
    SamplerType<:AbstractSampler,
    AlgType<:InferenceAlgorithm
}
    if resume_from === nothing
        return sample(model, Sampler(alg, model), N; progress=PROGRESS[], kwargs...)
    else
        return resume(resume_from, N)
    end
end

function Interface.psample(
    model::ModelType,
    alg::AlgType,
    N::Integer,
    n_chains::Integer;
    kwargs...
) where {
    ModelType<:Sampleable,
    SamplerType<:AbstractSampler,
    AlgType<:InferenceAlgorithm
}
    return psample(GLOBAL_RNG, model, alg, N, n_chains; progress=false, kwargs...)
end

function Interface.psample(
    rng::AbstractRNG,
    model::ModelType,
    alg::AlgType,
    N::Integer,
    n_chains::Integer;
    kwargs...
) where {
    ModelType<:Sampleable,
    SamplerType<:AbstractSampler,
    AlgType<:InferenceAlgorithm
}
    return psample(rng, model, Sampler(alg, model), N, n_chains; progress=false, kwargs...)
end

function Interface.sample_init!(
    ::AbstractRNG,
    model::Model,
    spl::Sampler,
    N::Integer;
    kwargs...
)
    # Resume the sampler.
    set_resume!(spl; kwargs...)

    # Set the parameters to a starting value.
    initialize_parameters!(spl; kwargs...)
end

function Interface.sample_end!(
    ::AbstractRNG,
    ::Model,
    spl::AbstractSampler,
    ::Integer,
    ::Vector{TransitionType};
    kwargs...
) where {TransitionType<:AbstractTransition}
    # Silence the default API function.
end

function initialize_parameters!(
    spl::AbstractSampler;
    init_theta::Union{Nothing,Array{<:Any,1}}=nothing,
    verbose::Bool=false,
    kwargs...
)
    # Get `init_theta`
    if init_theta !== nothing
        verbose && @info "Using passed-in initial variable values" init_theta
        # Convert individual numbers to length 1 vector; `ismissing(v)` is needed as `size(missing)` is undefined`
        init_theta = [ismissing(v) || size(v) == () ? [v] : v for v in init_theta]
        # Flatten `init_theta`
        init_theta_flat = foldl(vcat, map(vec, init_theta))
        # Create a mask to inidicate which values are not missing
        theta_mask = map(x -> !ismissing(x), init_theta_flat)
        # Get all values
        theta = spl.state.vi[spl]
        @assert length(theta) == length(init_theta_flat) "Provided initial value doesn't match the dimension of the model"
        # Update those which are provided (i.e. not missing)
        theta[theta_mask] .= init_theta_flat[theta_mask]
        # Update in `vi`
        spl.state.vi[spl] = theta
    end
end

##########################
# Chain making utilities #
##########################

function _params_to_array(ts::Vector{T}, spl::Sampler) where {T<:AbstractTransition}
    names = Set{String}()
    dicts = Vector{Dict{String, Any}}()

    # Extract the parameter names and values from each transition.
    for t in ts
        nms, vs = flatten_namedtuple(t.θ)
        push!(names, nms...)

        # Convert the names and values to a single dictionary.
        d = Dict{String, Any}()
        for (k, v) in zip(nms, vs)
            d[k] = v
        end
        push!(dicts, d)
    end

    # Convert the set to an ordered vector so the parameter ordering
    # is deterministic.
    ordered_names = collect(names)
    vals = Matrix{Union{Real, Missing}}(undef, length(ts), length(ordered_names))

    # Place each element of all dicts into the returned value matrix.
    for i in eachindex(dicts)
        for (j, key) in enumerate(ordered_names)
            vals[i,j] = get(dicts[i], key, missing)
        end
    end

    return ordered_names, vals
end

function flatten_namedtuple(nt::NamedTuple{pnames}) where {pnames}
    vals = Vector{Real}()
    names = Vector{AbstractString}()

    for k in pnames
        v = nt[k]
        if length(v) == 1
            flatten(names, vals, string(k), v)
        else
            for (vnval, vn) in zip(v[1], v[2])
                flatten(names, vals, vn, vnval)
            end
        end
    end

    return names, vals
end

function flatten(names, value :: AbstractArray, k :: String, v)
    if isa(v, Number)
        name = k
        push!(value, v)
        push!(names, name)
    elseif isa(v, Array)
        for i = eachindex(v)
            if isa(v[i], Number)
                name = string(ind2sub(size(v), i))
                name = replace(name, "(" => "[");
                name = replace(name, ",)" => "]");
                name = replace(name, ")" => "]");
                name = k * name
                isa(v[i], Nothing) && println(v, i, v[i])
                push!(value, v[i])
                push!(names, name)
            elseif isa(v[i], AbstractArray)
                name = k * string(ind2sub(size(v), i))
                flatten(names, value, name, v[i])
            else
                error("Unknown var type: typeof($v[i])=$(typeof(v[i]))")
            end
        end
    else
        error("Unknown var type: typeof($v)=$(typeof(v))")
    end
    return
end

ind2sub(v, i) = Tuple(CartesianIndices(v)[i])

function get_transition_extras(ts::Vector{T}) where T<:AbstractTransition
    # Get the extra field names from the sampler state type.
    # This handles things like :lp or :weight.
    extra_params = additional_parameters(T)

    # Get the values of the extra parameters.
    local extra_names
    all_vals = []

    # Iterate through each transition.
    for t in ts
        extra_names = String[]
        vals = []

        # Iterate through each of the additional field names
        # in the struct.
        for p in extra_params
            # Check whether the field contains a NamedTuple,
            # in which case we need to iterate through each
            # key/value pair.
            prop = getproperty(t, p)
            if prop isa NamedTuple
                for (k, v) in pairs(prop)
                    push!(extra_names, string(k))
                    push!(vals, v)
                end
            else
                push!(extra_names, string(p))
                push!(vals, prop)
            end
        end
        push!(all_vals, vals)
    end

    # Convert the vector-of-vectors to a matrix.
    valmat = [all_vals[i][j] for i in 1:length(ts), j in 1:length(all_vals[1])]

    return extra_names, valmat
end

# Default Chains constructor.
function Chains(
    rng::AbstractRNG,
    model::ModelType,
    spl::Sampler,
    N::Integer,
    ts::Vector{T};
    discard_adapt::Bool=true,
    save_state=false,
    kwargs...
) where {ModelType<:Sampleable, T<:AbstractTransition}
    # Check if we have adaptation samples.
    if discard_adapt && :n_adapts in fieldnames(typeof(spl.alg))
        ts = ts[(spl.alg.n_adapts+1):end]
    end

    # Convert transitions to array format.
    # Also retrieve the variable names.
    nms, vals = _params_to_array(ts, spl)

    # Get the values of the extra parameters in each Transition struct.
    extra_params, extra_values = get_transition_extras(ts)

    # Extract names & construct param array.
    nms = string.(vcat(nms..., string.(extra_params)...))
    parray = hcat(vals, extra_values)

    # If the state field has average_logevidence or final_logevidence, grab that.
    le = missing
    if :average_logevidence in fieldnames(typeof(spl.state))
        le = getproperty(spl.state, :average_logevidence)
    elseif :final_logevidence in fieldnames(typeof(spl.state))
        le = getproperty(spl.state, :final_logevidence)
    end

    # Check whether to invlink! the varinfo
    if islinked(spl.state.vi, spl)
        invlink!(spl.state.vi, spl)
    end

    # Set up the info tuple.
    info = if save_state
        (range = rng,
        model = model,
        spl = spl)
    else
        NamedTuple()
    end

    # Chain construction.
    return Chains(
        parray,
        string.(nms),
        deepcopy(TURING_INTERNAL_VARS);
        evidence=le,
        info=info
    )
end

function save(c::Chains, spl::AbstractSampler, model, vi, samples)
    nt = NamedTuple{(:spl, :model, :vi, :samples)}((spl, model, deepcopy(vi), samples))
    return setinfo(c, merge(nt, c.info))
end

function resume(c::Chains, n_iter::Int; kwargs...)
    @assert !isempty(c.info) "[Turing] cannot resume from a chain without state info"
    return sample(
        c.info[:range],
        c.info[:model],
        c.info[:spl],
        n_iter;    # this is actually not used
        resume_from=c,
        reuse_spl_n=n_iter,
        kwargs...
    )
end

function set_resume!(
    s::Sampler;
    resume_from::Union{Chains, Nothing}=nothing,
    kwargs...
)
    # If we're resuming, grab the sampler info.
    if resume_from !== nothing
        s = resume_from.info[:spl]
    end
end

function split_var_str(var_str)
    ind = findfirst(c -> c == '[', var_str)
    inds = Vector{String}[]
    if ind === nothing
        return var_str, inds
    end
    sym = var_str[1:ind-1]
    ind = length(sym)
    while ind < length(var_str)
        ind += 1
        @assert var_str[ind] == '['
        push!(inds, String[])
        while var_str[ind] != ']'
            ind += 1
            if var_str[ind] == '['
                ind2 = findnext(c -> c == ']', var_str, ind)
                push!(inds[end], strip(var_str[ind:ind2]))
                ind = ind2+1
            else
                ind2 = findnext(c -> c == ',' || c == ']', var_str, ind)
                push!(inds[end], strip(var_str[ind:ind2-1]))
                ind = ind2
            end
        end
    end
    return sym, inds
end

#########################
# Default sampler state #
#########################

"""
A blank `AbstractSamplerState` that contains only `VarInfo` information.
"""
mutable struct SamplerState{VIType<:VarInfo} <: AbstractSamplerState
    vi :: VIType
end

#######################################
# Concrete algorithm implementations. #
#######################################

include("hmc.jl")
include("mh.jl")
include("is.jl")
include("AdvancedSMC.jl")
include("gibbs.jl")
include("../contrib/inference/sghmc.jl")
include("../contrib/inference/AdvancedSMCExtensions.jl")

################
# Typing tools #
################

for alg in (:SMC, :PG, :PMMH, :IPMCMC, :MH, :IS)
    @eval getspace(::$alg{space}) where {space} = space
    @eval getspace(::Type{<:$alg{space}}) where {space} = space
end
for alg in (:HMC, :HMCDA, :NUTS, :SGLD, :SGHMC)
    @eval getspace(::$alg{<:Any, space}) where {space} = space
    @eval getspace(::Type{<:$alg{<:Any, space}}) where {space} = space
end
getspace(::Gibbs) = Tuple{}()
getspace(::Type{<:Gibbs}) = Tuple{}()

@inline floatof(::Type{T}) where {T <: Real} = typeof(one(T)/one(T))
@inline floatof(::Type) = Real

@inline Turing.Core.get_matching_type(spl::Turing.Sampler, vi::Turing.RandomVariables.VarInfo, ::Type{T}) where {T <: AbstractFloat} = floatof(eltype(vi, spl))
@inline Turing.Core.get_matching_type(spl::Turing.Sampler{<:Hamiltonian}, vi::Turing.RandomVariables.VarInfo, ::Type{TV}) where {T, N, TV <: Array{T, N}} = Array{Turing.Core.get_matching_type(spl, vi, T), N}
@inline Turing.Core.get_matching_type(spl::Turing.Sampler{<:Union{PG, SMC}}, vi::Turing.RandomVariables.VarInfo, ::Type{TV}) where {T, N, TV <: Array{T, N}} = TArray{T, N}

## Fallback functions

# utility funcs for querying sampler information
require_gradient(spl::Sampler) = false
require_particles(spl::Sampler) = false

assume(spl::Sampler, dist::Distribution) =
error("Turing.assume: unmanaged inference algorithm: $(typeof(spl))")

observe(spl::Sampler, weight::Float64) =
error("Turing.observe: unmanaged inference algorithm: $(typeof(spl))")

## Default definitions for assume, observe, when sampler = nothing.
function assume(spl::A,
    dist::Distribution,
    vn::VarName,
    vi::VarInfo) where {A<:Union{SampleFromPrior, SampleFromUniform}}

    if haskey(vi, vn)
        r = vi[vn]
    else
        r = isa(spl, SampleFromUniform) ? init(dist) : rand(dist)
        push!(vi, vn, r, dist, spl)
    end
    # NOTE: The importance weight is not correctly computed here because
    #       r is genereated from some uniform distribution which is different from the prior
    # acclogp!(vi, logpdf_with_trans(dist, r, istrans(vi, vn)))

    r, logpdf_with_trans(dist, r, istrans(vi, vn))

end

function assume(spl::A,
    dists::Vector{T},
    vn::VarName,
    var::Any,
    vi::VarInfo) where {T<:Distribution, A<:Union{SampleFromPrior, SampleFromUniform}}

    @assert length(dists) == 1 "Turing.assume only support vectorizing i.i.d distribution"
    dist = dists[1]
    n = size(var)[end]

    vns = map(i -> VarName(vn, "[$i]"), 1:n)

    if haskey(vi, vns[1])
        rs = vi[vns]
    else
        rs = isa(spl, SampleFromUniform) ? init(dist, n) : rand(dist, n)

        if isa(dist, UnivariateDistribution) || isa(dist, MatrixDistribution)
            for i = 1:n
                push!(vi, vns[i], rs[i], dist, spl)
            end
            @assert size(var) == size(rs) "Turing.assume: variable and random number dimension unmatched"
            var = rs
        elseif isa(dist, MultivariateDistribution)
            for i = 1:n
                push!(vi, vns[i], rs[:,i], dist, spl)
            end
            if isa(var, Vector)
                @assert length(var) == size(rs)[2] "Turing.assume: variable and random number dimension unmatched"
                for i = 1:n
                    var[i] = rs[:,i]
                end
            elseif isa(var, Matrix)
                @assert size(var) == size(rs) "Turing.assume: variable and random number dimension unmatched"
                var = rs
            else
                @error("Turing.assume: unsupported variable container"); error()
            end
        end
    end

    # acclogp!(vi, sum(logpdf_with_trans(dist, rs, istrans(vi, vns[1]))))

    var, sum(logpdf_with_trans(dist, rs, istrans(vi, vns[1])))

end

observe(::Nothing,
        dist::T,
        value::Any,
        vi::VarInfo) where T = observe(SampleFromPrior(), dist, value, vi)

function observe(spl::A,
    dist::Distribution,
    value::Any,
    vi::VarInfo) where {A<:Union{SampleFromPrior, SampleFromUniform}}

    vi.num_produce += one(vi.num_produce)
    Turing.DEBUG && @debug "dist = $dist"
    Turing.DEBUG && @debug "value = $value"

    # acclogp!(vi, logpdf(dist, value))
    logpdf(dist, value)
end

function observe(spl::A,
    dists::Vector{T},
    value::Any,
    vi::VarInfo) where {T<:Distribution, A<:Union{SampleFromPrior, SampleFromUniform}}

    @assert length(dists) == 1 "Turing.observe only support vectorizing i.i.d distribution"
    dist = dists[1]
    @assert isa(dist, UnivariateDistribution) ||
        isa(dist, MultivariateDistribution) "Turing.observe: vectorizing matrix distribution is not supported"
    if isa(dist, UnivariateDistribution)  # only univariate distributions support broadcast operation (logpdf.) by Distributions.jl
        # acclogp!(vi, sum(logpdf.(Ref(dist), value)))
        sum(logpdf.(Ref(dist), value))
    else
        # acclogp!(vi, sum(logpdf(dist, value)))
        sum(logpdf(dist, value))
    end

end

##############
# Utilities  #
##############

getspace(spl::Sampler) = getspace(typeof(spl))
getspace(::Type{<:Sampler{Talg}}) where {Talg} = getspace(Talg)

end # module


models = (
    ARDRegressor,
    BayesianRidgeRegressor,
    ElasticNetRegressor,
    ElasticNetCVRegressor,
    HuberRegressor,
    LarsRegressor,
    LarsCVRegressor,
    LassoRegressor,
    LassoCVRegressor,
    LassoLarsRegressor,
    LassoLarsCVRegressor,
    LassoLarsICRegressor,
    LinearRegressor,
    OrthogonalMatchingPursuitRegressor,
    OrthogonalMatchingPursuitCVRegressor,
    PassiveAggressiveRegressor,
    RANSACRegressor,
    RidgeRegressor,
    RidgeCVRegressor,
    SGDRegressor,
    TheilSenRegressor
    )

fparams = (
    ARDRegressor=(:coef, :intercept, :alpha, :lambda, :sigma, :scores),
    BayesianRidgeRegressor=(:coef, :intercept, :alpha, :lambda, :sigma, :scores),
    ElasticNetRegressor=(:coef, :intercept),
    ElasticNetCVRegressor=(:coef, :intercept, :l1_ratio, :mse_path, :alphas),
    HuberRegressor=(:coef, :intercept, :scale, :outliers),
    LarsRegressor=(:coef, :intercept, :alphas, :active, :coef_path),
    LarsCVRegressor=(:coef, :intercept, :alpha, :alphas, :cv_alphas, :mse_path, :coef_path),
    LassoRegressor=(:coef, :intercept),
    LassoCVRegressor=(:coef, :intercept, :alpha, :alphas, :mse_path, :dual_gap),
    LassoLarsRegressor=(:coef, :intercept, :alphas, :active, :coef_path),
    LassoLarsCVRegressor=(:coef, :intercept, :coef_path, :alpha, :alphas, :cv_alphas, :mse_path),
    LassoLarsICRegressor=(:coef, :intercept, :alpha),
    LinearRegressor=(:coef, :intercept),
    OrthogonalMatchingPursuitRegressor=(:coef, :intercept),
    OrthogonalMatchingPursuitCVRegressor=(:coef, :intercept, :n_nonzero_coefs),
    PassiveAggressiveRegressor=(:coef, :intercept),
    RANSACRegressor=(:estimator, :n_trials, :inlier_mask, :n_skips_no_inliers, :n_skips_invalid_data, :n_skips_invalid_model),
    RidgeRegressor=(:coef, :intercept),
    RidgeCVRegressor=(:coef, :intercept, :alpha, :cv_values),
    SGDRegressor=(:coef, :intercept, :average_coef, :average_intercept),
    TheilSenRegressor=(:coef, :intercept, :breakdown, :n_subpopulation)
    )

@testset "Fit/Predict" begin
    # check roughly that models return sensible results
    X, y, ls = simple_regression()
    for mod in models
        if mod in (ElasticNetRegressor, LassoRegressor, LassoLarsRegressor,
                   OrthogonalMatchingPursuitRegressor, OrthogonalMatchingPursuitCVRegressor
                   )
            continue
        end
        m = mod()
        # ratio of rmse to LS
        r, f = test_regression(m, X, y, ls)
        @test r < 3 # no more than twice as bad
        @test keys(MB.fitted_params(m, f)) == getproperty(fparams, Symbol(mod))
    end
    # these models are pretty bad because their parameters are not set
    for mod in (ElasticNetRegressor, LassoRegressor, LassoLarsRegressor)
        m = mod()
        r, f = test_regression(m, X, y, ls)
        # these models are pretty bad because their parameters are bad
        @test r < 15
        @test keys(MB.fitted_params(m, f)) == getproperty(fparams, Symbol(mod))
    end
    # Both of those are a bit specific (more aimed at finding sparsity mask)
    # and if used "just like that" will show unsightly error messages
    # OrthogonalMatchingPursuitCVRegressor
    # OrthogonalMatchingPursuitRegressor
end

@testset "Docstrings" begin
    for mod in models
        m = mod()
        @test !isempty(MB.docstring(m))
    end
end


export Melody

using ..NoteSequences: DEFAULT_STEPS_PER_BAR, DEFAULT_STEPS_PER_QUARTER
using ..NoteSequences: DEFAULT_QPM, DEFAULT_TPQ
using MacroTools: @forward

"""
    Melody <: Any

Melody is an intermediate representation for monophonic musical sequences.
Melody events are integers in the range [-2, 127].
The negative values are `MELODY_NO_EVENt`, `MELODY_NOTE_OFF` and they are special events.
Note on events are the non-negative values from [0, 127] and they represent a midi pitch.

A melody note starts at a midi pitch value (non-negative). The note is sustained through
the following `MELODY_NO_EVENT`s until another midi pitch is reached or a `MELODY_NOTE_OFF`
event is reached.

## Fields
* `events::Vector{Int}`: The monophonic melody events in the sequence.
* `steps_per_bar::Int`: Number of steps per bar of music.
* `steps_per_quarter::Int`: Number of steps per quarter note.
* `startstep::Int`: The first step of the melody in the sequence.
* `endstep::Int`: The last step of the melody in the sequence.
"""
mutable struct Melody
    events::Vector{Int}
    steps_per_bar::Int
    steps_per_quarter::Int
    startstep::Int
    endstep::Int

    function Melody(;events::Vector{Int}=Int[],
                     startstep::Int=0,
                     steps_per_bar::Int=DEFAULT_STEPS_PER_BAR,
                     steps_per_quarter::Int=DEFAULT_STEPS_PER_QUARTER)

        if !isempty(events)
            for event in events
                isoutofrange(event) &&
                    throw(error("Melody event is out of range"))
            end

            # Up to the first event, replace MELODY_NOTE_OFF with MELODY_NO_EVENT
            # to prevent unwanted note-offs.
            for (i, event) in enumerate(events)
                event ∉ [MELODY_NO_EVENT, MELODY_NOTE_OFF] && break
                events[i] = MELODY_NO_EVENT
            end
        end

        endstep = startstep + length(events)
        new(events, steps_per_bar, steps_per_quarter, startstep, endstep)
    end
end

@forward Melody.events Base.getindex, Base.length, Base.iterate, Base.lastindex

function Base.push!(melody::Melody, event::Int)
    isoutofrange(event) && throw(error("Melody event is out of range"))
    push!(melody.events, event)
    melody.endstep += 1
    melody
end

function Base.append!(melody::Melody, events::Vector{Int})
    append!(melody.events, events)
    melody.endstep += length(events)
    melody
end

Base.setindex!(m::Melody, event::Int, idx::Int) = setindex!(m.events, event, idx)

"""
    Melody(qns::NoteSequence,
                search_start_step::Int=0,
                gapbars::Int=1,
                ignore_polyphony::Bool=false,
                pad_end::Bool=false,
                instrument::Int=1)

Convert the given quantized notesequence to a melody. The melody is extracted starting from
`search_start_step`. The melody extraction will be ended when there is a silence for more than
`gapbars` amount of bars in music.

If `ignore_polyphony` is true, only the highest pitch is kept when multiple pitches are encountered.
Otherwise, an error is thrown.

0 velocity notes are ignored when extracting the melody. If `pad_end` is true, the melody is padded with
`MELODY_NO_EVENt` until the end of the bar.
"""
function Melody(qns::NoteSequence,
                search_start_step::Int=0,
                gapbars::Int=1,
                ignore_polyphony::Bool=false,
                pad_end::Bool=false,
                instrument::Int=1)

    !qns.isquantized && throw(error("NoteSequence must be relative quantized."))

    quarters_per_beat = 4 / qns.timesignatures[1].denominator
    quarters_per_bar = quarters_per_beat * qns.timesignatures[1].numerator
    steps_per_bar = quarters_per_bar * qns.steps_per_quarter
    steps_per_bar % 1 != 0 && throw(error("Steps per bar must be an integer"))

    melody = Melody()
    melody.steps_per_bar = round(steps_per_bar)
    melody.steps_per_quarter = qns.steps_per_quarter

    notes = [note for note in qns.notes
             if note.instrument == instrument && note.start_time >= search_start_step]

    isempty(notes) && return

    sort!(notes, by=note -> (note.start_time, -note.pitch))

    initial_distance = (notes[1].start_time - search_start_step)
    melody_start_step = notes[1].start_time - initial_distance % melody.steps_per_bar

    for note in notes
        note.velocity == 0 && continue

        note_start_step = note.start_time - melody_start_step
        note_end_step = note.end_time - melody_start_step

        if isempty(melody.events)
            addnote!(melody, note.pitch, note_start_step, note_end_step)
            continue
        end

        laston, lastoff = last_onoff_events(melody)

        on_distance = note_start_step - laston
        off_distance = note_start_step - lastoff

        if on_distance == 0
            if ignore_polyphony
                continue # Keeps the highest note
            else
                throw(error("More than one note is found at the same time."))
            end
        elseif on_distance < 0
            throw(error("Notes not in ascending order. This is caused due to polyphonic melody."))
        end

        gapsteps = gapbars * melody.steps_per_bar

        # End the melody if there is a silence of `gap_bars` or More
        !isempty(melody) && off_distance >= gapsteps && break

        addnote!(melody, note.pitch, note_start_step, note_end_step)
    end

    isempty(melody) && return

    melody.startstep = melody_start_step
    melody[end] == MELODY_NOTE_OFF && pop!(melody.events)

    melodylength = length(melody)
    if pad_end
        melodylength += (-melodylength % melody.steps_per_bar)
    end
    setlength!(melody, melodylength)

    melody
end

"""
    getnotesequence(melody::Melody;
                    velocity::Int=100,
                    instrument::Int=1,
                    program::Int=0,
                    sequence_start_time::Int=0,
                    qpm::Float64=DEFAULT_QPM)

Convert the given melody to a `NoteSequence`. The notes events will have the same `velocity`,
`program` and `instrument`. The melody will start at `sequence_start_time` in the sequence
and will have a tempo of `qpm` (quarter notes per minute).
"""
function getnotesequence(melody::Melody;
                         velocity::Int=100,
                         instrument::Int=1,
                         program::Int=0,
                         sequence_start_time::Int=0,
                         qpm::Float64=DEFAULT_QPM)

    seconds_per_step = 60 / qpm / melody.steps_per_quarter
    ns = NoteSequence()
    push!(ns.tempos, NoteSequences.Tempo(0, qpm))
    ns.tpq = DEFAULT_TPQ

    sequence_start_time += melody.startstep * seconds_per_step
    note_is_playing = false
    local current_seqnote

    for (step, note) in enumerate(melody)
        seconds = step * seconds_per_step + sequence_start_time
        new_note_start_time = second2tick(seconds)

        if MIN_MIDI_PITCH <= note <= MAX_MIDI_PITCH
            if note_is_playing
                # End the sustained note
                current_seqnote.end_time = new_note_start_time
            end

            current_seqnote = NoteSequences.SeqNote(note, velocity,
                                                    new_note_start_time, 0,
                                                    instrument, program)

            push!(ns.notes, current_seqnote)
            note_is_playing = true

        elseif note == MELODY_NOTE_OFF
            if note_is_playing
                current_seqnote.end_time = new_note_start_time
                note_is_playing = false
            end
        end
    end

    if note_is_playing
        current_seqnote.end_time = second2tick(length(melody) * seconds_per_step + sequence_start_time)
    end

    if !isempty(ns.notes)
        ns.total_time = ns.notes[end].end_time
    end

    ns
end

"""
    setlength!(melody::Melody, steps::Int)

Set the length of the melody to `steps`. If the given `steps` is greater than
the length of the melody, the melody will be padded with `MELODY_NO_EVENT`s to the right.
If `steps` is smaller than the length of the melody, the melody will be truncated.
"""
function setlength!(melody::Melody, steps::Int)
    oldlength = length(melody)

    if steps > length(melody)
        padding = fill(MELODY_NO_EVENT, (steps - length(melody)))
        append!(melody, padding)
    else
        melody.events = melody.events[1:steps]
    end

    melody.endstep = melody.startstep + steps

    if steps > oldlength
        for i = oldlength:-1:1
            if melody[i] == MELODY_NOTE_OFF
                break
            elseif melody[i] != MELODY_NO_EVENT
                melody[oldlength] = MELODY_NOTE_OFF
            end
        end
    end

    melody
end

"""
    addnote!(melody::Melody, pitch::Int, startstep::Int, endstep::Int)

Adds the given note to the melody at `startstep` and `endstep`.
The `endstep` will be set to a `MELODY_NOTE_OFF`.
"""
function addnote!(melody::Melody, pitch::Int, startstep::Int, endstep::Int)
    startstep >= endstep && throw(error("End step must be greater than start step."))
    setlength!(melody, endstep)
    melody[startstep] = pitch
    melody[endstep] = MELODY_NOTE_OFF

    for i = (startstep + 1):endstep
        melody[i] = MELODY_NO_EVENT
    end

    melody
end

"""
    last_onoff_events(melody::Melody)

Return the indices of the last midi pitch and `MELODY_NOTE_OFF` in the sequence.
"""
function last_onoff_events(melody::Melody)
    lastoff = length(melody)
    for i = (length(melody)-1):-1:-1
        if melody[i] == MELODY_NOTE_OFF
            lastoff = i
        end
        melody[i] >= MIN_MIDI_PITCH && return (i, lastoff)
    end
end

isoutofrange(event::Int) = !(MIN_MELODY_EVENT <= event <= MAX_MELODY_EVENT)


"""
Code declaring SpectralTimeSeriesCommonWavelengths and their abstract versions.

Author: Eric Ford
Created: August 2020
"""

""" Abstract type for a time series of spectra that share a common wavelength grid. """
abstract type AbstractSpectralTimeSeriesCommonWavelengths <: AbstractSpectra1D   end

""" Time series of spectra that share a common wavelength grid. """
struct SpectralTimeSeriesCommonWavelengths{T1<:Real,T2<:Real,T3<:Real,AA1<:AbstractArray{T1,1},AA2<:AbstractArray{T2,2},AA3<:AbstractArray{T3,2},
            AA4<:AbstractArray{UnitRange{Int64},1}, InstT<:AbstractInstrument } <: AbstractSpectralTimeSeriesCommonWavelengths
    λ::AA1
    flux::AA2
    var::AA3
    chunk_map::AA4
    inst::InstT
    metadata::MetadataT
end

function SpectralTimeSeriesCommonWavelengths(λ::A1, flux::A2, var::A3, chunk_map::A4, inst::InstT;
        metadata::MetadataT = MetadataT() ) where {
          T1<:Real, T2<:Real, T3<:Real, A1<:AbstractArray{T1,1}, A2<:AbstractArray{T2,2}, A3<:AbstractArray{T3,2},
          A4<:AbstractArray{UnitRange{Int64},1}, InstT<:AbstractInstrument }
    #println("len(λ) = ", length(λ))
    #println("size(flux) = ", size(flux))
    @assert length(λ) == size(flux,1)
    @assert length(λ) == size(var,1)
    @assert 1 <= length(λ)
    SpectralTimeSeriesCommonWavelengths{eltype(λ),eltype(flux),eltype(var),typeof(λ),typeof(flux),typeof(var),typeof(chunk_map),typeof(inst)}(λ,flux,var,chunk_map,inst,metadata)
end

"""   make_spectral_time_series_common_wavelengths_with_selected_times( input, time_idx )
Returns a SpectralTimeSeriesCommonWavelengths, retaining only those times and spectra specified by time_idx.
"""
function make_spectral_time_series_common_wavelengths_with_selected_times(input::STSCWT, time_idx::AbstractVector{T1} ) where { STSCWT<:AbstractSpectralTimeSeriesCommonWavelengths, T1<:Integer }
    @assert minimum(time_idx) >= 1
    @assert maximum(time_idx) <= size(input.flux,2)
    metadata = length(input.metadata) == size(input.flux,2) ? input.metadata[time_idx] : MetadataT()
    SpectralTimeSeriesCommonWavelengths(input.λ, input.flux[:,time_idx], input.var[:,time_idx], input.chunk_map, input.inst, metadata=metadata )
end


models = (
    LogisticClassifier,
    LogisticCVClassifier,
    PassiveAggressiveClassifier,
    PerceptronClassifier,
    RidgeClassifier,
    RidgeCVClassifier,
    SGDClassifier,
    ProbabilisticSGDClassifier,
    )

fparams = (
    LogisticClassifier=(:classes, :coef, :intercept),
    LogisticCVClassifier=(:classes, :coef, :intercept, :Cs, :l1_ratios, :coefs_paths, :scores, :C, :l1_ratio),
    PassiveAggressiveClassifier=(:coef, :intercept),
    PerceptronClassifier=(:coef, :intercept),
    RidgeClassifier=(:coef, :intercept),
    RidgeCVClassifier=(:coef, :intercept),
    SGDClassifier=(:coef, :intercept),
    ProbabilisticSGDClassifier=(:coef, :intercept)
    )

@testset "Fit/Predict" begin
    X, y = simple_binaryclf()
    for mod in models
        m = mod()
        acc, f = test_clf(m, X, y)
        @test acc >= 0.75
        @test keys(MB.fitted_params(m, f)) == getproperty(fparams, Symbol(mod))
    end
end

@testset "Docstrings" begin
    for mod in models
        m = mod()
        @test !isempty(MB.docstring(m))
    end
end


function Transform(arg0::AbsoluteDate, arg1::AngularCoordinates)
    return Transform((AbsoluteDate, AngularCoordinates), arg0, arg1)
end

function Transform(arg0::AbsoluteDate, arg1::PVCoordinates)
    return Transform((AbsoluteDate, PVCoordinates), arg0, arg1)
end

function Transform(arg0::AbsoluteDate, arg1::Rotation)
    return Transform((AbsoluteDate, Rotation), arg0, arg1)
end

function Transform(arg0::AbsoluteDate, arg1::Rotation, arg2::Vector3D)
    return Transform((AbsoluteDate, Rotation, Vector3D), arg0, arg1, arg2)
end

function Transform(arg0::AbsoluteDate, arg1::Rotation, arg2::Vector3D, arg3::Vector3D)
    return Transform((AbsoluteDate, Rotation, Vector3D, Vector3D), arg0, arg1, arg2, arg3)
end

function Transform(arg0::AbsoluteDate, arg1::Transform, arg2::Transform)
    return Transform((AbsoluteDate, Transform, Transform), arg0, arg1, arg2)
end

function Transform(arg0::AbsoluteDate, arg1::Vector3D)
    return Transform((AbsoluteDate, Vector3D), arg0, arg1)
end

function Transform(arg0::AbsoluteDate, arg1::Vector3D, arg2::Vector3D)
    return Transform((AbsoluteDate, Vector3D, Vector3D), arg0, arg1, arg2)
end

function Transform(arg0::AbsoluteDate, arg1::Vector3D, arg2::Vector3D, arg3::Vector3D)
    return Transform((AbsoluteDate, Vector3D, Vector3D, Vector3D), arg0, arg1, arg2, arg3)
end

function freeze(obj::Transform)
    return jcall(obj, "freeze", Transform, ())
end

function get_acceleration(obj::Transform)
    return jcall(obj, "getAcceleration", Vector3D, ())
end

function get_angular(obj::Transform)
    return jcall(obj, "getAngular", AngularCoordinates, ())
end

function get_cartesian(obj::Transform)
    return jcall(obj, "getCartesian", PVCoordinates, ())
end

function get_date(obj::Transform)
    return jcall(obj, "getDate", AbsoluteDate, ())
end

function get_inverse(obj::Transform)
    return jcall(obj, "getInverse", Transform, ())
end

function get_jacobian(obj::Transform, arg0::CartesianDerivativesFilter, arg1::Vector{Vector{jdouble}})
    return jcall(obj, "getJacobian", void, (CartesianDerivativesFilter, Vector{Vector{jdouble}}), arg0, arg1)
end

function get_rotation(obj::Transform)
    return jcall(obj, "getRotation", Rotation, ())
end

function get_rotation_acceleration(obj::Transform)
    return jcall(obj, "getRotationAcceleration", Vector3D, ())
end

function get_rotation_rate(obj::Transform)
    return jcall(obj, "getRotationRate", Vector3D, ())
end

function get_translation(obj::Transform)
    return jcall(obj, "getTranslation", Vector3D, ())
end

function get_velocity(obj::Transform)
    return jcall(obj, "getVelocity", Vector3D, ())
end

function interpolate(::Type{Transform}, arg0::AbsoluteDate, arg1::CartesianDerivativesFilter, arg2::AngularDerivativesFilter, arg3::Collection)
    return jcall(Transform, "interpolate", Transform, (AbsoluteDate, CartesianDerivativesFilter, AngularDerivativesFilter, Collection), arg0, arg1, arg2, arg3)
end

function interpolate(obj::TimeInterpolable, arg0::AbsoluteDate, arg1::Collection)
    return jcall(obj, "interpolate", TimeInterpolable, (AbsoluteDate, Collection), arg0, arg1)
end

function interpolate(obj::Transform, arg0::AbsoluteDate, arg1::Stream)
    return jcall(obj, "interpolate", Transform, (AbsoluteDate, Stream), arg0, arg1)
end

function shifted_by(obj::Transform, arg0::jdouble)
    return jcall(obj, "shiftedBy", Transform, (jdouble,), arg0)
end

function transform_line(obj::Transform, arg0::Line)
    return jcall(obj, "transformLine", Line, (Line,), arg0)
end

function transform_position(obj::Transform, arg0::FieldVector3D)
    return jcall(obj, "transformPosition", FieldVector3D, (FieldVector3D,), arg0)
end

function transform_position(obj::Transform, arg0::Vector3D)
    return jcall(obj, "transformPosition", Vector3D, (Vector3D,), arg0)
end

function transform_pv_coordinates(obj::Transform, arg0::FieldPVCoordinates)
    return jcall(obj, "transformPVCoordinates", FieldPVCoordinates, (FieldPVCoordinates,), arg0)
end

function transform_pv_coordinates(obj::Transform, arg0::PVCoordinates)
    return jcall(obj, "transformPVCoordinates", PVCoordinates, (PVCoordinates,), arg0)
end

function transform_pv_coordinates(obj::Transform, arg0::TimeStampedFieldPVCoordinates)
    return jcall(obj, "transformPVCoordinates", TimeStampedFieldPVCoordinates, (TimeStampedFieldPVCoordinates,), arg0)
end

function transform_pv_coordinates(obj::Transform, arg0::TimeStampedPVCoordinates)
    return jcall(obj, "transformPVCoordinates", TimeStampedPVCoordinates, (TimeStampedPVCoordinates,), arg0)
end

function transform_vector(obj::Transform, arg0::FieldVector3D)
    return jcall(obj, "transformVector", FieldVector3D, (FieldVector3D,), arg0)
end

function transform_vector(obj::Transform, arg0::Vector3D)
    return jcall(obj, "transformVector", Vector3D, (Vector3D,), arg0)
end


module PedestrianAvoidancePOMDP

using Random
using Distributions
using AutomotiveDrivingModels
using AutoViz
using AutomotiveSensors
using AutomotivePOMDPs
using Parameters
using StaticArrays
using GridInterpolations
using POMDPs
using POMDPModelTools
using POMDPPolicies
using QMDP
using JLD2
using Reel
using EmergencyBrakingSystem

include("pomdp_types.jl")
include("spaces.jl")
include("transition.jl")
include("observation.jl")
include("belief.jl")
include("policy.jl")
include("pedestrian_pomdp.jl")
include("helpers.jl")
include("rendering.jl")
include("pedestrian_pomdp_emergency_system.jl")
include("evaluation_scenario.jl")

export
    PedestrianAvoidancePOMDPFrenet,
    PedestrianAvoidanceSystem,

    SingleOCFPOMDP,
    SingleOCFUpdater,
    SingleOCFBelief,
    SingleOCFAction,
    SingleOCFState,
    CrosswalkEnv,
    ObservationCallback,
    animate_record,
    generate_scenario,
    evaluateScenarioMetric,
    evaluateScenariosMetric

end


__precompile__()
module easyClipper

    const depsfile = joinpath(@__DIR__, "..", "deps", "deps.jl")

    if isfile(depsfile)
        include(depsfile)
    else
        error("Clipper not build correctly. Please run Pkg.build(\"Clipper\")")
    end

    function __init__()
        check_deps()
    end

    export PolyType, PolyTypeSubject, PolyTypeClip,
           ClipType, ClipTypeIntersection, ClipTypeUnion, ClipTypeDifference, ClipTypeXor,
           PolyFillType, PolyFillTypeEvenOdd, PolyFillTypeNonZero, PolyFillTypePositive, PolyFillTypeNegative,
           JoinType, JoinTypeSquare, JoinTypeRound, JoinTypeMiter,
           EndType, EndTypeClosedPolygon, EndTypeClosedLine, EndTypeOpenSquare, EndTypeOpenRound, EndTypeOpenButt,
           Clip, add_path!, add_paths!, execute, clear!, get_bounds,
           IntPoint, IntRect, orientation, area, pointinpolygon, ClipperOffset,
           PolyNode, execute_pt, contour, ishole, contour, children

    @enum PolyType PolyTypeSubject=0 PolyTypeClip=1

    @enum ClipType ClipTypeIntersection=0 ClipTypeUnion=1 ClipTypeDifference=2 ClipTypeXor=3

    @enum PolyFillType PolyFillTypeEvenOdd=0 PolyFillTypeNonZero=1 PolyFillTypePositive=2 PolyFillTypeNegative=3

    @enum JoinType JoinTypeSquare=0 JoinTypeRound=1 JoinTypeMiter=2

    @enum EndType EndTypeClosedPolygon=0 EndTypeClosedLine=1 EndTypeOpenSquare=2 EndTypeOpenRound=3 EndTypeOpenButt=4

    struct IntPoint
        X::Int64
        Y::Int64
    end

    mutable struct PolyNode{T}
        contour::Vector{T}
        hole::Bool
        open::Bool
        children::Vector{PolyNode{T}}
        parent::PolyNode{T}
        PolyNode{T}(a,b,c) where {T} = new{T}(a,b,c)
        function PolyNode{T}(a,b,c,d) where T
            p = new{T}(a,b,c,d)
            p.parent = p
            return p
        end
        PolyNode{T}(a,b,c,d,e) where {T} = new{T}(a,b,c,d,e)
    end

    Base.convert(::Type{PolyNode{T}}, x::PolyNode{T}) where {T} = x
    function Base.convert(::Type{PolyNode{S}}, x::PolyNode{T}) where {S,T}
        parent(x) !== x && error("must convert a top-level PolyNode (i.e. a PolyTree).")

        pn = PolyNode{S}(convert(Vector{S}, contour(x)), ishole(x), isopen(x))
        pn.children = [PolyNode(y,pn) for y in children(x)]
        pn.parent = pn
    end
    function PolyNode(x::PolyNode, parent::PolyNode{S}) where S
        pn = PolyNode{S}(contour(x), ishole(x), isopen(x))
        pn.children = [PolyNode(y,pn) for y in children(x)]
        pn.parent = parent
        pn
    end

    @inline ishole(x::PolyNode) = x.hole
    @inline Base.isopen(x::PolyNode) = x.open
    @inline contour(x::PolyNode) = x.contour
    @inline children(x::PolyNode) = x.children
    @inline Base.parent(x::PolyNode) = x.parent

    function Base.show(io::IO, node::PolyNode)
        if parent(node) === node
            print(io, "Top-level PolyNode with $(length(children(node))) immediate children.")
        else
            if isopen(node)
                print(io, "Open ")
            else
                print(io, "Closed ")
            end
            print(io, "PolyNode ")
            ishole(node) && print(io, "(hole) ")
            println(io, "with contour:")
            show(io, contour(node))
            println(io, "")
            print(io, "...and $(length(children(node))) immediate children.")
        end
    end

    function Base.show(io::IO, point::IntPoint)
      print(io, "[$(point.X),$(point.Y)]")
    end

    function append!(outputArray::Ptr{Cvoid}, polyIndex::Csize_t, point::IntPoint)
        ourArray = unsafe_pointer_to_objref(outputArray)::Vector{Vector{IntPoint}}

        while (polyIndex + 1) > length(ourArray)
          push!(ourArray, Vector{IntPoint}())
        end

        push!(ourArray[polyIndex + 1], point)
    end

    # private
    function appendpn!(jl_node::Ptr{Cvoid}, point::IntPoint)
        node = unsafe_pointer_to_objref(jl_node)::PolyNode{IntPoint}
        push!(contour(node), point)
    end

    # private
    function newnode(outputTree::Ptr{Cvoid}, ishole::Bool, isopen::Bool)
        tree = unsafe_pointer_to_objref(outputTree)::PolyNode{IntPoint}
        node = PolyNode{IntPoint}(IntPoint[], ishole, isopen, PolyNode{IntPoint}[], tree)
        push!(children(tree), node)
        pointer_from_objref(node)
    end

    #==============================================================#
  	# Static functions
  	#==============================================================#
    function orientation(path::Vector{IntPoint})
        ccall((:orientation, cclipper), Cuchar, (Ptr{IntPoint}, Csize_t),
            path,
            length(path)) == 1 ? true : false
    end

    function area(path::Vector{IntPoint})
        ccall((:area, cclipper), Float64, (Ptr{IntPoint}, Csize_t),
            path,
            length(path))
    end

    function pointinpolygon(pt::IntPoint, path::Vector{IntPoint})
        ccall((:pointinpolygon, cclipper), Cint, (IntPoint, Ptr{IntPoint}, Csize_t),
            pt,
            path,
            length(path))
    end

    #==============================================================#
  	# Clipper object
  	#==============================================================#
    mutable struct Clip
        clipper_ptr::Ptr{Cvoid}

        function Clip()
            clipper = new(ccall((:get_clipper, cclipper), Ptr{Cvoid}, ()))
            finalizer(c -> ccall((:delete_clipper, cclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr), clipper)
            clipper
        end
    end

    function add_path!(c::Clip, path::Vector{IntPoint}, polyType::PolyType, closed::Bool)
        ccall((:add_path, cclipper), Cuchar, (Ptr{Cvoid}, Ptr{IntPoint}, Csize_t, Cint, Cuchar),
              c.clipper_ptr,
              path,
              length(path),
              Int(polyType),
              closed) == 1 ? true : false
    end

    function add_paths!(c::Clip, paths::Vector{Vector{IntPoint}}, polyType::PolyType, closed::Bool)
        lengths = Vector{UInt64}()
        for path in paths
            push!(lengths, length(path))
        end

        ccall((:add_paths, cclipper), Cuchar, (Ptr{Cvoid}, Ptr{Ptr{IntPoint}}, Ptr{Csize_t}, Csize_t, Cint, Cuchar),
              c.clipper_ptr,
              paths,
              lengths,
              length(paths),
              Int(polyType),
              closed) == 1 ? true : false
    end

    function execute(c::Clip, clipType::ClipType, subjFillType::PolyFillType, clipFillType::PolyFillType)
        polys = Vector{Vector{IntPoint}}()

        result = ccall((:execute, cclipper), Cuchar, (Ptr{Cvoid}, Cint, Cint, Cint, Any, Ptr{Cvoid}),
                        c.clipper_ptr,
                        Int(clipType),
                        Int(subjFillType),
                        Int(clipFillType),
                        polys,
                        @cfunction(append!, Any, (Ptr{Cvoid}, Csize_t, IntPoint)))

        return result == 1 ? true : false, polys
    end

    function execute_pt(c::Clip, clipType::ClipType, subjFillType::PolyFillType, clipFillType::PolyFillType)
        pt = PolyNode{IntPoint}(IntPoint[], false, false, PolyNode{IntPoint}[])

        result = ccall((:execute_pt, cclipper), Cuchar,
            (Ptr{Cvoid}, Cint, Cint, Cint, Any, Ptr{Cvoid}, Ptr{Cvoid}),
            c.clipper_ptr,
            Int(clipType),
            Int(subjFillType),
            Int(clipFillType),
            pt,
            @cfunction(newnode, Ptr{Cvoid}, (Ptr{Cvoid}, Bool, Bool)),
            @cfunction(appendpn!, Any, (Ptr{Cvoid}, IntPoint)))

        return result == 1 ? true : false, pt
    end

    function clear!(c::Clip)
        ccall((:clear, cclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr)
    end

    mutable struct IntRect
        left::Int64
        top::Int64
        right::Int64
        bottom::Int64
    end

    function get_bounds(c::Clip)
        ccall((:get_bounds, cclipper), IntRect, (Ptr{Cvoid}, ), c.clipper_ptr)
    end

    #==============================================================#
  	# ClipperOffset object
  	#==============================================================#
    mutable struct ClipperOffset
        clipper_ptr::Ptr{Cvoid}

        function ClipperOffset(miterLimit::Float64 = 2.0, roundPrecision::Float64 = 0.25)
            clipper = new(ccall((:get_clipper_offset, cclipper), Ptr{Cvoid}, (Cdouble, Cdouble), miterLimit, roundPrecision))
	    finalizer(c -> ccall((:delete_clipper_offset, cclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr), clipper)

            clipper
        end
    end

    function add_path!(c::ClipperOffset, path::Vector{IntPoint}, joinType::JoinType, endType::EndType)
        ccall((:add_offset_path, cclipper), Cvoid, (Ptr{Cvoid}, Ptr{IntPoint}, Csize_t, Cint, Cint),
              c.clipper_ptr,
              path,
              length(path),
              Int(joinType),
              Int(endType))
    end

    function add_paths!(c::ClipperOffset, paths::Vector{Vector{IntPoint}}, joinType::JoinType, endType::EndType)
        lengths = Vector{UInt64}()
        for path in paths
            push!(lengths, length(path))
        end

        ccall((:add_offset_paths, cclipper), Cvoid, (Ptr{Cvoid}, Ptr{Ptr{IntPoint}}, Ptr{Csize_t}, Csize_t, Cint, Cint),
              c.clipper_ptr,
              paths,
              lengths,
              length(paths),
              Int(joinType),
              Int(endType))
    end

    function clear!(c::ClipperOffset)
        ccall((:clear_offset, cclipper), Cvoid, (Ptr{Cvoid},), c.clipper_ptr)
    end

    function execute(c::ClipperOffset, delta::Float64)
        polys = Vector{Vector{IntPoint}}()
        result = ccall((:execute_offset, cclipper), Cvoid, (Ptr{Cvoid}, Cdouble, Any, Ptr{Cvoid}),
                        c.clipper_ptr,
                        delta,
                        polys,
                        @cfunction(append!, Any, (Ptr{Cvoid}, Csize_t, IntPoint)))

        return polys
    end

    include("conversion.jl")
    include("utilities.jl")
end


export PerfRNN, generate

using StatsBase
using Flux: gate

struct PerfRNN
    model
    encoder::PerformanceOneHotEncoding
    num_velocitybins::Int
end

"""
    generate(perfrnn::PerfRNN, performance::Performance)

Generate `PerformanceEvent`s by sampling from a PerformanceRNN model.
"""
function generate(perfrnn::PerfRNN;
                  primer::Performance=Performance(100, velocity_bins=perfrnn.num_velocitybins),
                  numsteps=3000,
                  as_notesequence=false)

    model, encoder = perfrnn.model, perfrnn.encoder
    Flux.reset!(model)

    performance = deepcopy(primer)

    if isempty(performance)
        push!(performance, encoder.defaultevent)
    end

    # Primer is already numsteps or longer
    if performance.numsteps >= numsteps
        return performance
    end

    indices = map(event -> encode_event(event, encoder), performance)
    inputs = map(index -> Flux.onehot(index, encoder.labels), indices)

    outputs = model.(inputs)
    out = wsample(encoder.labels, softmax(outputs[end]))
    push!(performance, decode_event(out, encoder))

    while performance.numsteps < numsteps
        input = Flux.onehot(out, encoder.labels)
        logits = model(input)
        out = wsample(encoder.labels, softmax(logits))
        push!(performance, decode_event(out, encoder))
    end

    ns = getnotesequence(performance)
    as_notesequence == true && return ns
    midifile(ns)
end

# Replace Flux's LSTMCell with BasicLSTMCell from TensorFlow 1.
# It's implementation is slightly different.
# https://github.com/tensorflow/tensorflow/blob/v2.4.1/tensorflow/python/keras/layers/legacy_rnn/rnn_cell_impl.py#L786-L787
# https://github.com/FluxML/Flux.jl/blob/master/src/layers/recurrent.jl#L141-L142
# The replacement is required because the pre-trained models use BasicLSTMCell.
struct BasicLSTMCell{A,V,S}
  Wi::A
  Wh::A
  b::V
  state0::S
end

function BasicLSTMCell(in::Integer, out::Integer;
                  init = Flux.glorot_uniform,
                  initb = zeros,
                  init_state = zeros)
  cell = BasicLSTMCell(init(out * 4, in), init(out * 4, out), initb(out * 4), (init_state(out,1), init_state(out,1)))
  cell.b[gate(out, 2)] .= 1
  return cell
end

function (m::BasicLSTMCell)((h, c), x) where {A,V,T}
  b, o = m.b, size(h, 1)
  g = m.Wi*x .+ m.Wh*h .+ b
  input = σ.(gate(g, o, 1))
  cell = tanh.(gate(g, o, 2))
  forget = σ.(gate(g, o, 3))
  output = σ.(gate(g, o, 4))
  c = forget .* c .+ input .* cell
  h′ = output .* tanh.(c)
  sz = size(x)
  return (h′, c), reshape(h′, :, sz[2:end]...)
end

Flux.@functor BasicLSTMCell

Base.show(io::IO, l::BasicLSTMCell) =
  print(io, "BasicLSTMCell(", size(l.Wi, 2), ", ", size(l.Wi, 1)÷4, ")")

"""
    BasicLSTM(in::Integer, out::Integer)
Implements the BasicLSTMCell from TensorFlow 1
https://github.com/tensorflow/tensorflow/blob/v2.4.1/tensorflow/python/keras/layers/legacy_rnn/rnn_cell_impl.py#L648-L814
"""
BasicLSTM(a...; ka...) = Flux.Recur(BasicLSTMCell(a...; ka...))
Flux.Recur(m::BasicLSTMCell) = Flux.Recur(m, m.state0)


#!/usr/bin/env julia

using Distributed
addprocs(${ncpus})

@everywhere using FlashWeave

using CSV
using DataFrames

# data
data_path = "${otu_file}"
new_data_path = "${otu_file.baseName}.tsv"
if data_path != new_data_path
    cp(data_path, new_data_path, force=true, follow_symlinks=true)
end
metadata_path = "${sample_metadata}"
metadata = CSV.read(metadata_path, DataFrame; transpose=true)
new_metadata_path = "${sample_metadata.baseName}_transposed.tsv"
CSV.write(new_metadata_path, metadata; delim="\\t")

# options
sensitive = ${sensitive}
heterogeneous = ${heterogeneous}
FDR = ${fdr_correction}

netw_results = learn_network(
new_data_path,
new_metadata_path,
sensitive=sensitive,
heterogeneous=heterogeneous,
FDR=FDR,
transposed=true,
)

# You need to use networkx to export the .gml to table
output_file = "${meta.id}_network.gml"
save_network(output_file, netw_results)


#####
# some common components for Prioritized Experience Replay based methods
#####

const PERLearners = Union{PrioritizedDQNLearner,RainbowLearner,IQNLearner}

function extract_experience(t::AbstractTrajectory, learner::PERLearners)
    s = learner.stack_size
    h = learner.update_horizon
    n = learner.batch_size
    γ = learner.γ

    # 1. sample indices based on priority
    valid_ind_range =
        isnothing(s) ? (1:(length(t[:terminal])-h)) : (s:(length(t[:terminal])-h))
    if haskey(t, :priority)
        inds = Vector{Int}(undef, n)
        priorities = Vector{Float32}(undef, n)
        for i in 1:n
            ind, p = sample(learner.rng, t[:priority])
            while ind ∉ valid_ind_range
                ind, p = sample(learner.rng, t[:priority])
            end
            inds[i] = ind
            priorities[i] = p
        end
    else
        inds = rand(learner.rng, valid_ind_range, n)
        priorities = nothing
    end

    next_inds = inds .+ h

    # 2. extract SARTS
    states = consecutive_view(t[:state], inds; n_stack = s)
    actions = consecutive_view(t[:action], inds)
    next_states = consecutive_view(t[:state], next_inds; n_stack = s)

    if haskey(t, :legal_actions_mask)
        legal_actions_mask = consecutive_view(t[:legal_actions_mask], inds)
        next_legal_actions_mask = consecutive_view(t[:next_legal_actions_mask], inds)
    else
        legal_actions_mask = nothing
        next_legal_actions_mask = nothing
    end

    consecutive_rewards = consecutive_view(t[:reward], inds; n_horizon = h)
    consecutive_terminals = consecutive_view(t[:terminal], inds; n_horizon = h)
    rewards, terminals = zeros(Float32, n), fill(false, n)

    rewards = discount_rewards_reduced(
        consecutive_rewards,
        γ;
        terminal = consecutive_terminals,
        dims = 1,
    )
    terminals = mapslices(any, consecutive_terminals; dims = 1) |> vec

    inds,
    (
        states = states,
        legal_actions_mask = legal_actions_mask,
        actions = actions,
        rewards = rewards,
        terminals = terminals,
        next_states = next_states,
        next_legal_actions_mask = next_legal_actions_mask,
        priorities = priorities,
    )
end

function RLBase.update!(p::QBasedPolicy{<:PERLearners}, t::AbstractTrajectory)
    learner = p.learner
    length(t[:terminal]) < learner.min_replay_history && return

    learner.update_step += 1

    if learner.update_step % learner.target_update_freq == 0
        copyto!(learner.target_approximator, learner.approximator)
    end

    learner.update_step % learner.update_freq == 0 || return

    inds, experience = extract_experience(t, p.learner)

    if haskey(t, :priority)
        priorities = update!(p.learner, experience)
        t[:priority][inds] .= priorities
    else
        update!(p.learner, experience)
    end
end

function (agent::Agent{<:QBasedPolicy{<:PERLearners}})(::RLCore.Training{PostActStage}, env)
    push!(agent.trajectory; reward = get_reward(env), terminal = get_terminal(env))
    if haskey(agent.trajectory, :priority)
        push!(agent.trajectory; priority = agent.policy.learner.default_priority)
    end
    nothing
end


include(joinpath("tasks", "task_types.jl"))
include(joinpath("tasks", "product_tasks.jl"))
include(joinpath("tasks", "identity.jl"))
include(joinpath("tasks", "position_around_point.jl"))
include(joinpath("tasks", "distance_from_point.jl"))
include(joinpath("tasks", "distance_from_sphere_surface.jl"))
include(joinpath("tasks", "distance_sphere_to_sphere.jl"))
include(joinpath("tasks", "distance_sphere_to_cup.jl"))
include(joinpath("tasks", "distance_sphere_to_cylinder.jl"))
include(joinpath("tasks", "distance_sphere_to_box.jl"))
include(joinpath("tasks", "distance_sphere_to_line.jl"))
include(joinpath("tasks", "angular_distance.jl"))
include(joinpath("tasks", "angular_position_around_point.jl"))
include(joinpath("tasks", "coordinate.jl"))
include(joinpath("tasks", "coordinate_distance.jl"))
include(joinpath("tasks", "arm", "joint_to_link_position.jl"))
include(joinpath("tasks", "arm", "link_sphere_pair_distance.jl"))
include(joinpath("tasks", "arm", "link_sphere_position.jl"))
include(joinpath("tasks", "arm", "link_frame_transform.jl"))

Base.eltype(::Type{TaskMapX{A,B,S}}) where {A,B,S} = S

domain_manifold(task_map::Union{TaskMap{M,N},TaskMapT{M,N}}) where {M,N} = M
codomain_manifold(task_map::Union{TaskMap{M,N},TaskMapT{M,N}}) where {M,N} = N
domain_manifold(task::TaskX) = domain_manifold(task.task_map)
codomain_manifold(task::TaskX) = codomain_manifold(task.task_map)
domain_manifold(M::Manifold) = M

domain_coord_rep(task_map::TaskMapT) = domain_coord_rep(task_map.base_map)
codomain_coord_rep(task_map::TaskMapT) = codomain_coord_rep(task_map.base_map)

default_coord_rep(::TaskX) = ChartRep()

choose_chart_emb(xme, taskx::Union{TaskX,TaskMapX}, CM, CN) =
    choose_chart_emb(xme, base_task_map(taskx), CM, CN)
choose_chart_chart(xm, taskx::Union{TaskX,TaskMapX}, CM, CN) =
    choose_chart_chart(xm, base_task_map(taskx), CM, CN)

choose_chart_emb(xme, task_map::TaskMap, CM, CN) =
    choose_chart_emb(chart_choice_coord_rep(CN), xme, task_map, CM, CN)
choose_chart_chart(xm, task_map::TaskMap, CM, CN) =
    choose_chart_chart(chart_choice_coord_rep(CN), xm, task_map, CM, CN)

choose_chart_emb(::EmbRep, xme, task_map::TaskMap, CM, CN) =
    choose_chart_emb(EmbRep(), task_map_emb(xme, task_map, CM, CN), CN)
choose_chart_emb(::ChartRep, xme, task_map::TaskMap, CM, CN) =
    choose_chart_chart(ChartRep(), task_map_emb_chart(xme, task_map, CM, CN), CN)
choose_chart_chart(::EmbRep, xm, task_map::TaskMap, CM, CN) =
    choose_chart_emb(EmbRep(), task_map_chart_emb(xm, task_map, CM, CN), CN)
choose_chart_chart(::ChartRep, xm, task_map::TaskMap, CM, CN) =
    choose_chart_chart(ChartRep(), task_map_chart(xm, task_map, CM, CN), CN)

choose_chart(::EmbRep, xme, taskx::Union{TaskX,TaskMapX}, CM, CN) =
    choose_chart_emb(xme, taskx, CM, CN)
choose_chart(::ChartRep, xm, taskx::Union{TaskX,TaskMapX}, CM, CN) =
    choose_chart_chart(xm, taskx, CM, CN)

base_task_map(task::TaskX) = base_task_map(task.task_map)
base_task_map(task_map::TaskMap) = task_map
base_task_map(task_map::TaskMapX) = task_map.base_map

task_map_emb(pme, task_map::TaskMapX, arg...) =
    task_map_emb(domain_coord_rep(task_map), codomain_coord_rep(task_map), pme, task_map, arg...)
task_map_emb_chart(pme, task_map::TaskMapX, arg...) =
    task_map_emb_chart(domain_coord_rep(task_map), codomain_coord_rep(task_map),
    pme, task_map, arg...)
task_map_chart_emb(pm, task_map::TaskMapX, arg...) =
    task_map_chart_emb(domain_coord_rep(task_map), codomain_coord_rep(task_map),
    pm, task_map, arg...)
task_map_chart(pm, task_map::TaskMapX, arg...) =
    task_map_chart(domain_coord_rep(task_map), codomain_coord_rep(task_map), pm, task_map, arg...)

task_map_emb(pme, task::TaskX, arg...) = task_map_emb(pme, task.task_map, arg...)
task_map_emb_chart(pme, task::TaskX, arg...) = task_map_emb_chart(pme, task.task_map, arg...)
task_map_chart_emb(pm, task::TaskX, arg...) = task_map_chart_emb(pm, task.task_map, arg...)
task_map_chart(pm, task::TaskX, arg...) = task_map_chart(pm, task.task_map, arg...)

base_task_map_emb(pme, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_map_emb(pme, base_task_map(taskx), arg...)
base_task_map_emb_chart(pme, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_map_emb_chart(pme, base_task_map(taskx), arg...)
base_task_map_chart_emb(pm, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_map_chart_emb(pm, base_task_map(taskx), arg...)
base_task_map_chart(pm, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_map_chart(pm, base_task_map(taskx), arg...)

## No chart specified
# task_map_emb(::EmbRep, ::EmbRep, pme, task_map::TaskMap) # Should be defined for specific task
# Throw out given charts as needed; base emb map should be defined
task_map_emb(::EmbRep, ::EmbRep, pme, task_map::TaskMap, arg...) =
    task_map_emb(EmbRep(), EmbRep(), pme, task_map)
task_map_emb(::CoordinateRep, ::CoordinateRep, pme, task_map) =
    throw(ArgumentError("Chart(s) not specified"))

task_map_emb_chart(::CoordinateRep, ::CoordinateRep, pme, task_map) =
    throw(ArgumentError("Chart not specified"))
task_map_chart_emb(::CoordinateRep, ::CoordinateRep, pm, task_map) =
    throw(ArgumentError("Chart not specified"))
task_map_chart(::CoordinateRep, ::CoordinateRep, pm, task_map) =
    throw(ArgumentError("Charts not specified"))

## One chart specified
function task_map_emb(::EmbRep, ::ChartRep, pme, task_map::TaskMap, CN)
    pn = task_map_emb_chart(pme, task_map, CN)
    pne = chart_to_emb(pn, CN)
end
function task_map_emb(::ChartRep, ::EmbRep, pme, task_map::TaskMap, CM)
    pm = emb_to_chart(pme, CM)
    pne = task_map_chart_emb(pm, task_map, CM)
end
task_map_emb(::ChartRep, ::ChartRep, pme, task_map, C) =
    throw(ArgumentError("Second chart not specified"))

function task_map_emb_chart(::EmbRep, ::EmbRep, pme, task_map::TaskMap{M,N},
        CN::Chart{J,N}) where {M,N,J}
    pne = task_map_emb(pme, task_map)
    pn = emb_to_chart(pne, CN)
end
# Should be defined for specific task:
# task_map_emb_chart(::EmbRep, ::ChartRep, pm, task_map::TaskMap, CN)
task_map_emb_chart(::ChartRep, ::EmbRep, pme, task_map, C) =
    throw(ArgumentError("Second chart not specified"))
task_map_emb_chart(::ChartRep, ::ChartRep, pme, task_map, C) =
    throw(ArgumentError("Second chart not specified"))

function task_map_chart_emb(::EmbRep, ::EmbRep, pm, task_map::TaskMap{M,N},
        CM::Chart{I,M}) where {M,N,I}
    pme = chart_to_emb(pm, CM)
    pne = task_map_emb(pme, task_map)
end
task_map_chart_emb(::EmbRep, ::ChartRep, pm, task_map, C) =
    throw(ArgumentError("Second chart not specified"))
# Should be defined for specific task:
# task_map_chart_emb(::ChartRep, ::EmbRep, pm, task_map::TaskMap, C)
task_map_chart_emb(::ChartRep, ::ChartRep, pm, task_map, C) =
    throw(ArgumentError("Second chart not specified"))

task_map_chart(::CoordinateRep, ::CoordinateRep, pm, task_map, C) =
    throw(ArgumentError("Second chart not specified"))

## Both charts specified
task_map_emb(::EmbRep, ::ChartRep, pme, task_map::TaskMap, CM, CN) =
    task_map_emb(pme, task_map, CN)
task_map_emb(::ChartRep, ::EmbRep, pme, task_map::TaskMap, CM, CN) =
    task_map_emb(pme, task_map, CM)
function task_map_emb(::ChartRep, ::ChartRep, pme, task_map::TaskMap, CM, CN)
    pm = emb_to_chart(pme, CM)
    pn = task_map_chart(pm, task_map, CM, CN)
    pne = chart_to_emb(pn, CN)
end

task_map_emb_chart(::EmbRep, ::EmbRep, pme, task_map::TaskMap, CM, CN) =
    task_map_emb_chart(pme, task_map, CN)
task_map_emb_chart(::EmbRep, ::ChartRep, pme, task_map::TaskMap, CM, CN) =
    task_map_emb_chart(pme, task_map, CN)
function task_map_emb_chart(::ChartRep, ::EmbRep, pme, task_map::TaskMap{M,N}, CM,
        CN::Chart{J,N}) where {M,N,J}
    pm = emb_to_chart(pme, CM)
    pne = task_map_chart_emb(pm, task_map, CM)
    pn = emb_to_chart(pne, CN)
end
function task_map_emb_chart(::ChartRep, ::ChartRep, pme, task_map::TaskMap, CM, CN)
    pm = emb_to_chart(pme, CM)
    pn = task_map_chart(pm, task_map, CM, CN)
end

task_map_chart_emb(::EmbRep, ::EmbRep, pm, task_map::TaskMap, CM, CN) =
    task_map_chart_emb(pm, task_map, CM)
function task_map_chart_emb(::EmbRep, ::ChartRep, pm, task_map::TaskMap{M,N}, CM::Chart{I,M},
        CN) where {M,N,I}
    pme = chart_to_emb(pm, CM)
    pn = task_map_emb_chart(pme, task_map, CN)
    pne = chart_to_emb(pn, CN)
end
task_map_chart_emb(::ChartRep, ::EmbRep, pm, task_map::TaskMap, CM, CN) =
    task_map_chart_emb(pm, task_map, CM)
function task_map_chart_emb(::ChartRep, ::ChartRep, pm, task_map::TaskMap, CM, CN)
    pn = task_map_chart(pm, task_map, CM, CN)
    pne = chart_to_emb(pn, CN)
end

function task_map_chart(::EmbRep, ::EmbRep, pm, task_map::TaskMap{M,N}, CM::Chart{I,M},
        CN::Chart{J,N}) where {M,N,I,J}
    pme = chart_to_emb(pm, CM)
    pne = task_map_emb(pme, task_map)
    pn = emb_to_chart(pne, CN)
end
function task_map_chart(::EmbRep, ::ChartRep, pm, task_map::TaskMap{M,N}, CM::Chart{I,M},
        CN) where {M,N,I}
    pme = chart_to_emb(pm, CM)
    pn = task_map_emb_chart(pme, task_map, CN)
end
function task_map_chart(::ChartRep, ::EmbRep, pm, task_map::TaskMap{M,N}, CM,
        CN::Chart{J,N}) where {M,N,J}
    pne = task_map_chart_emb(pm, task_map, CM)
    pn = emb_to_chart(pne, CN)
end
# Should be defined for specific task
# task_map_chart(::ChartRep, ::ChartRep, pm, task_map::TaskMap, CM, CN)

function task_differential_map_emb(pme, wme, task_map::TaskMap, arg...)
    pne = task_map_emb(pme, task_map, arg...)
    ∂pne_∂pme = task_jacobian_emb(pme, task_map, arg...)
    wne = ∂pne_∂pme*wme
    pne, wne
end

function task_differential_map_emb_chart(pme, wme, task_map::TaskMap, arg...)
    pn = task_map_emb_chart(pme, task_map, arg...)
    ∂pn_∂pme = task_jacobian_emb_chart(pme, task_map, arg...)
    wn = ∂pn_∂pme*wme
    pn, wn
end

function task_differential_map_chart_emb(pm, wm, task_map::TaskMap, arg...)
    pne = task_map_chart_emb(pm, task_map, arg...)
    ∂pne_∂pm = task_jacobian_chart_emb(pm, task_map, arg...)
    wne = ∂pne_∂pm*wm
    pne, wne
end

function task_differential_map_chart(pm, wm, task_map::TaskMap, arg...)
    pn = task_map_chart(pm, task_map, arg...)
    ∂pn_∂pm = task_jacobian_chart(pm, task_map, arg...)
    wn = ∂pn_∂pm*wm
    pn, wn
end

task_differential_map_emb(pme, wme, task::TaskX, arg...) =
    task_differential_map_emb(pme, wme, base_task_map(task), arg...)
task_differential_map_emb_chart(pme, wme, task::TaskX, arg...) =
    task_differential_map_emb_chart(pme, wme, base_task_map(task), arg...)
task_differential_map_chart_emb(pm, wme, task::TaskX, arg...) =
    task_differential_map_chart_emb(pm, wme, base_task_map(task), arg...)
task_differential_map_chart(pm, wme, task::TaskX, arg...) =
    task_differential_map_chart(pm, wme, base_task_map(task), arg...)

## Tangent bundle task maps
function task_map_emb(pme, task_map::TaskMapT{M,N}, arg...) where {M,N}
    xme, vme = tangent_vector_emb_splitview(pme, T{M})
    xne, vne = task_differential_map_emb(xme, vme, task_map.base_map, arg...)
    pne = [xne; vne]
end

function task_map_emb_chart(pme, task_map::TaskMapT{M,N}, arg...) where {M,N}
    xme, vme = tangent_vector_emb_splitview(pme, T{M})
    xn, vn = task_differential_map_emb_chart(xme, vme, task_map.base_map, arg...)
    pn = [xn; vn]
end

function task_map_chart_emb(pm, task_map::TaskMapT{M,N}, arg...) where {M,N}
    xm, vm = tangent_vector_chart_splitview(pm, T{M})
    xne, vne = task_differential_map_chart_emb(xm, vm, task_map.base_map, arg...)
    pne = [xne; vne]
end

function task_map_chart(pm, task_map::TaskMapT{M,N}, arg...) where {M,N}
    xm, vm = tangent_vector_chart_splitview(pm, T{M})
    xn, vn = task_differential_map_chart(xm, vm, task_map.base_map, arg...)
    pn = [xn; vn]
end

## Jacobians
task_jacobian_emb(pme, task_map::TaskMapX, arg...) =
    ForwardDiff.jacobian(pme -> task_map_emb(pme, task_map, arg...), pme)
task_jacobian_emb_chart(pme, task_map::TaskMapX, arg...) =
    ForwardDiff.jacobian(pme -> task_map_emb_chart(pme, task_map, arg...), pme)
task_jacobian_chart_emb(pm, task_map::TaskMapX, arg...) =
    ForwardDiff.jacobian(pm -> task_map_chart_emb(pm, task_map, arg...), pm)
task_jacobian_chart(pm, task_map::TaskMapX, arg...) =
    ForwardDiff.jacobian(pm -> task_map_chart(pm, task_map, arg...), pm)

base_task_jacobian_emb(pme, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_emb(pme, base_task_map(taskx), arg...)
base_task_jacobian_emb_chart(pme, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_emb_chart(pme, base_task_map(taskx), arg...)
base_task_jacobian_chart_emb(pm, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_chart_emb(pm, base_task_map(taskx), arg...)
base_task_jacobian_chart(pm, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_chart(pm, base_task_map(taskx), arg...)

task_jacobian_emb(pme, task::TaskX, arg...) = task_jacobian_emb(pme, task.task_map, arg...)
task_jacobian_emb_chart(pme, task::TaskX, arg...) =
    task_jacobian_emb_chart(pme, task.task_map, arg...)
task_jacobian_chart_emb(pm, task::TaskX, arg...) =
    task_jacobian_chart_emb(pm, task.task_map, arg...)
task_jacobian_chart(pm, task::TaskX, arg...) = task_jacobian_chart(pm, task.task_map, arg...)

## Jacobian Time Derivatives
base_task_jacobian_emb_dot(xme, vme, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_emb_dot(xme, vme, base_task_map(taskx), arg...)
base_task_jacobian_chart_dot(xm, vm, taskx::Union{TaskX,TaskMapX}, arg...) =
    task_jacobian_chart_dot(xm, vm, base_task_map(taskx), arg...)

task_jacobian_emb_dot(xme, vme, task::TaskX, arg...) =
    task_jacobian_emb_dot(xme, vme, task.task_map, arg...)
task_jacobian_chart_dot(xm, vm, task::TaskX, arg...) =
    task_jacobian_chart_dot(xm, vm, task.task_map, arg...)

function task_jacobian_emb_dot(xme, vme, task_map::BaseTaskMap, arg...)
    m, n = embdim(domain_manifold(task_map)), embdim(codomain_manifold(task_map))
    ∇xf = ForwardDiff.jacobian(xme -> SVector{m*n,eltype(xme)}(reshape(task_jacobian_emb(xme,
        task_map, arg...), m*n)), xme)::SArray{Tuple{m*n,m},eltype(xme),2,m*m*n}
    Jfdot_emb = reshape(∇xf*vme, Size(n,m))
end

function task_jacobian_chart_dot(xm, vm, task_map::BaseTaskMap, arg...)
    m, n = dim(domain_manifold(task_map)), dim(codomain_manifold(task_map))
    ∇xf = ForwardDiff.jacobian(xm -> SVector{m*n,eltype(xm)}(reshape(task_jacobian_chart(xm,
        task_map, arg...), m*n)), xm)::SArray{Tuple{m*n,m},eltype(xm),2,m*m*n}
    Jfdot = reshape(∇xf*vm, Size(n,m))
end

# Default coordinate representations for tasks on Rn
domain_coord_rep(::TaskMap{ℝ{m},N}) where {m,N} = EmbRep()
codomain_coord_rep(::TaskMap{M,ℝ{n}}) where {M,n} = EmbRep()

# Default home task charts for tasks on Rn
home_task_chart(::Task{<:TaskMap{M,ℝ{n}}}) where {M,n} = Chart{1,ℝ{n}}()
home_task_chart(::Task{<:TaskMapT{M,ℝ{n}}}) where {M,n} = Chart{1,ℝ{n}}()
home_task_chart(::TaskGDS{<:TaskMapT{M,ℝ{n}}}) where {M,n} = Chart{1,ℝ{n}}()
home_task_chart(::Task{<:PTM{<:TaskMap{M1,ℝ{n1}},<:TaskMap{M2,ℝ{n2}}}}
    ) where {M1,M2,n1,n2} = Chart{Tuple{1,1},PM{ℝ{n1},ℝ{n2}}}()
home_task_chart(::Task{<:PTMT{<:PTM{<:TaskMap{M1,ℝ{n1}},<:TaskMap{M2,ℝ{n2}}}}}
    ) where {M1,M2,n1,n2} = Chart{Tuple{1,1},PM{ℝ{n1},ℝ{n2}}}()
# Default home task charts for S1
home_task_chart(::Task{<:TaskMap{M,S1}}) where M = Chart{Angleπ,S1}()
home_task_chart(::Task{<:PTM{<:TaskMap{M1,ℝ{n}},<:TaskMap{M2,S1}}}
    ) where {M1,M2,n} = Chart{Tuple{1,Angleπ},PM{ℝ{n},S1}}()

## Task Weighting
task_weight(pn1, task::TaskX, C1) = 1. # Default weighting is 1 everywhere
task_weight(xn1, vn1, task::TaskX, C1) = 1.