__precompile__()

module EarthEngineREST

using NPZ
using Retry
using JSON3
using PyCall
using GeoArrays
using EarthEngine
using GeoDataFrames
using DocStringExtensions
import EarthEngine: Initialize
import GeoArrays: geotransform_to_affine

# define a constant pointer to the ee object
const serviceaccount = PyNULL()
const gauth = PyNULL()

# specify different urls and routings based on api version
const baseurl = "https://earthengine.googleapis.com"
const alphaapi = join([baseurl, "v1alpha"],"/")
const betaapi = join([baseurl, "v1beta"], "/")
const latestapi = betaapi

# create a lookup dictionary of api paths so users can use their preference
# add more as different versions become available
const apis = Dict("alpha" => alphaapi, "beta" => betaapi, "latest" => latestapi)

include("eeresttypes.jl")
include("eevalues.jl")
include("eeimages.jl")
include("eetables.jl")

"""
    _sendrequest(session::EESession, endpoint::AbstractString, data::Dict{Symbol,<:Any}; version::AbstractString = "latest", nretries::Integer = 4)

Private function for sending requests to EarthEngine REST API. Uses exponential backoff to retry requests if failed with non-fatal error.
"""
function _sendrequest(
    session::EESession,
    endpoint::AbstractString,
    data::Dict{Symbol,<:Any};
    version::AbstractString = "latest",
    nretries::Integer = 4
)

    urlparts = [apis[version], "projects", session.project, endpoint]
    url = join(urlparts, "/")

    @repeat nretries try
        response = session.auth.post(url = url, data = JSON3.write(data))

    catch e
        @delay_retry if e.status_code < 200 && e.status_code >= 500
        end
    end
end

"""
    typetodict(x::T)

Function to convert EarthEngineREST types to dictionary key,value pairs to encode
for API requests
"""
function typetodict(x::T)::Dict{Symbol,Any} where {T}
    fields = fieldnames(T)
    outdict = Dict{Symbol,Any}()
    for fn in fields
        subfield = getfield(x, fn)
        fn = String(fn)
        if length(fieldnames(typeof(subfield))) > 0
            if endswith(String(fn), "of")
                fn = replace(fn, "of" => "")
            end

            outdict[Symbol(fn)] = typetodict(subfield)
        else
            if endswith(fn, "of")
                fn = replace(fn, "of" => "")
            end

            outdict[Symbol(fn)] = subfield
        end
    end
    return outdict
end

"""
    extract_geotransform(x::AffineTransform)

Function to extract geotransform vector from AffineTransformation type
"""
function extract_geotransform(x::AffineTransform)
    af = typetodict(x)
    gt = [
        af[:translateX],
        af[:scaleX],
        af[:shearX],
        af[:translateY],
        af[:shearY],
        af[:scaleY],
    ]
end

"""
    extract_geotransform(x::PixelGrid)

Function to extract geotransform vector from PixelGrid type
"""
function extract_geotransform(x::PixelGrid)
    extract_geotransform(x.affineTransform)
end

"""
    extract_bbox(x::AffineTransform, griddims::GridDimensions)

Function to extract bounding box vector from AffineTransform type. Vector will be [W, S, E, N]
"""
function extract_bbox(x::AffineTransform, griddims::GridDimensions)
    geo_t = extract_geotransform(x)
    x_size = griddims.width
    y_size = griddims.height

    xmin = min(geo_t[1], geo_t[1] + x_size * geo_t[2])
    xmax = max(geo_t[1], geo_t[1] + x_size * geo_t[2])
    ymin = min(geo_t[4], geo_t[4] + y_size * geo_t[6])
    ymax = max(geo_t[4], geo_t[4] + y_size * geo_t[6])

    return [xmin, ymin, xmax, ymax]
end

"""
    extract_bbox(x::PixelGrid)

Function to extract bounding box vector from PixelGrid type. Vector will be [W, S, E, N]
"""
function extract_bbox(x::PixelGrid)
    extract_bbox(x.affineTransform,x.dimensions)
end

"""
    extract_affinemap(x::AffineTransform)

Function to extract AffineMap type from AffineTransform type.
"""
function extract_affinemap(x::AffineTransform)
    gt = extract_geotransform(x)
    return geotransform_to_affine(gt)
end

"""
    extract_affinemap(x::PixelGrid)

Function to extract AffineMap type from PixelGrid type.
"""
function extract_affinemap(x::PixelGrid)
    extract_affinemap(x.affineTransform)
end

function extract_lonslats(x::AffineTransform)
    gt = extract_geotransform()

end

"""
    extract_gridcoordinates(x::PixelGrid)

Function to convert PixelGrid to vectors of lat lon coordinates. Returns
a tuple of (lon::Vector{Float64}, lat::Vector{Float64}).
"""
function extract_gridcoordinates(x::PixelGrid)
    bbox = extract_bbox(x)
    gt = extract_geotransform(x)

    lons = collect(bbox[1]:abs(gt[2]):bbox[3])[1:end-1]
    lats = collect(bbox[2]:abs(gt[6]):bbox[4])[2:end]

    return lons, lats
end

"""
    Initialize(session::EESession)

Extends the `Initialize()` function to take an authenticated Earth Engine session
and use those credentials.
"""
function Initialize(session::EESession)
    EarthEngine.Initialize(session.auth.credentials)
end

export Initialize, EESession, PixelGrid, GridDimensions, AffineTransform, computepixels, computetable, computevalue

end # module


export KnetArray, KnetMatrix, KnetVector, KnetVecOrMat, DevArray, ka
import CUDA: CUDA, CuArray, device
import Base: Array, convert, unsafe_convert, pointer, copyto!
using Base: unsafe_wrap
# include("kptr.jl") ## KnetPtr

"""

    KnetArray{T}(undef,dims)
    KnetArray(a::AbstractArray)
    Array(k::KnetArray)

Container for GPU arrays that supports most of the AbstractArray interface.  The constructor
allocates a KnetArray in the currently active device, as specified by `CUDA.device()`.
KnetArrays and Arrays can be converted to each other as shown above, which involves copying
to and from the GPU memory.  Only Float32/64 KnetArrays are fully supported.

KnetArrays use the CUDA.jl package for allocation and some operations. Currently some of
the custom CUDA kernels that implement elementwise, broadcasting, and reduction operations
for KnetArrays work faster. Once these are improved in CUDA.jl, KnetArrays will be retired.

# Supported functions:

* Indexing: getindex, setindex! with the following index types:
  * 1-D: Real, Colon, OrdinalRange, AbstractArray{Real}, AbstractArray{Bool}, CartesianIndex, AbstractArray{CartesianIndex}, EmptyArray, KnetArray{Int32} (low level), KnetArray{0/1} (using float for BitArray) (1-D includes linear indexing of multidimensional arrays)
  * 2-D: (Colon,Union{Real,Colon,OrdinalRange,AbstractVector{Real},AbstractVector{Bool},KnetVector{Int32}}), (Union{Real,AbstractUnitRange,Colon}...) (in any order)
  * N-D: (Real...)

* Array operations: ==, !=, adjoint, argmax, argmin, cat, convert, copy, copyto!, deepcopy,
  display, eachindex, eltype, endof, fill!, findmax, findmin, first, hcat, isapprox,
  isempty, length, ndims, one, ones, permutedims, pointer, rand!, randn!, reshape, similar,
  size, stride, strides, summary, transpose, vcat, vec, zero.  (Boolean operators generate
  outputs with same type as inputs; no support for KnetArray{Bool}.)

* Unary functions with broadcasting: -, abs, abs2, acos, acosh, asin, asinh, atan, atanh,
  cbrt, ceil, cos, cosh, cospi, digamma, erf, erfc, erfcinv, erfcx, erfinv, exp, exp10,
  exp2, expm1, floor, gamma, lgamma, log, log10, log1p, log2, loggamma, one, round, sign,
  sin, sinh, sinpi, sqrt, tan, tanh, trigamma, trunc, zero

* Binary functions with broadcasting: !=, *, +, -, /, <, <=, ==, >, >=, ^, max, min

* Reduction operators: maximum, minimum, prod, sum

* Statistics: mean, std, stdm, var, varm

* Linear algebra: (*), axpy!, lmul!, norm, rmul!

* Knet extras: batchnorm, bce, bmm, cat1d, conv4, cpucopy, deconv4, dropout, elu, gpucopy,
  logistic, logp, logsoftmax, logsumexp, mat, nll, pool, relu, RNN, selu, sigm,
  softmax, unpool (Only 4D/5D, Float32/64 KnetArrays support conv4, pool, deconv4, unpool)

"""
mutable struct KnetArray{T,N} # <: AbstractArray{T,N} (TODO)
    ptr::KnetPtr
    dims::NTuple{N,Int}
end

# Note: I removed <: AbstractArray{T,N} after I painfully discovered some inefficient
# AbstractArray methods inherited unintentionally.  It is better to define a few extra
# methods to keep a tighter control on what methods exactly get called for KnetArrays.

# Aliases:
const KnetMatrix{T} = KnetArray{T,2}
const KnetVector{T} = KnetArray{T,1}
const KnetVecOrMat{T} = Union{KnetVector{T}, KnetMatrix{T}}
const DevArray{T,N} = Union{KnetArray{T,N},CuArray{T,N}}

# Constructors:
# Internal constructor defines KnetArray{T,N}(ptr,dims)

# These define KnetArray{T,N}(undef,dims) and KnetArray{T,N}(undef,d...)
KnetArray{T,N}(::UndefInitializer, d::Vararg{Int,N}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), d)
KnetArray{T,N}(::UndefInitializer, d::NTuple{N,Int}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), d)
KnetArray{T,N}(::UndefInitializer, d::Vararg{Integer,N}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), convert(NTuple{N,Int},d))
KnetArray{T,N}(::UndefInitializer, d::NTuple{N,Integer}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), convert(NTuple{N,Int},d))

# These define KnetArray{T}(undef,dims) and KnetArray{T}(undef,d...)
KnetArray{T}(::UndefInitializer, d::Vararg{Int,N}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), d)
KnetArray{T}(::UndefInitializer, d::NTuple{N,Int}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), d)
KnetArray{T}(::UndefInitializer, d::Vararg{Integer,N}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), convert(NTuple{N,Int},d))
KnetArray{T}(::UndefInitializer, d::NTuple{N,Integer}) where {T,N} = KnetArray{T,N}(KnetPtr(sizeof(T)*prod(d)), convert(NTuple{N,Int},d))

# KnetArray(::KnetArray) creates a copy, convert returns an alias if possible
KnetArray(A::KnetArray{T,N})    where {T,N}   = KnetArray{T,N}(A)
KnetArray{T}(A::KnetArray{S,N}) where {T,N,S} = KnetArray{T,N}(A)
KnetArray{T,N}(x::KnetArray{T,N}) where {T,N} = copyto!(KnetArray{T}(undef,x.dims), 1, x, 1, prod(x.dims))
KnetArray{T,N}(x::KnetArray{S,N}) where {T,N,S} = copyto!(KnetArray{T}(undef,x.dims), 1, convert(Array{T,N},x), 1, prod(x.dims))

# KnetArray(::AbstractArray)
KnetArray(A::AbstractArray{T,N})    where {T,N}   = KnetArray{T,N}(A)
KnetArray{T}(A::AbstractArray{S,N}) where {T,N,S} = KnetArray{T,N}(A)
KnetArray{T,N}(x::AbstractArray{S,N}) where {T,N,S} = copyto!(KnetArray{T}(undef,size(x)), 1, convert(Array{T,N},x), 1, length(x))

# Conversions:

# Array(::KnetArray)
Array(A::KnetArray{T,N})    where {T,N}   = Array{T,N}(A)
Array{T}(A::KnetArray{S,N}) where {T,N,S} = Array{T,N}(A)
Array{T,N}(x::KnetArray{S,N}) where {T,N,S} = convert(Array{T,N}, copyto!(Array{S}(undef,x.dims), 1, x, 1, prod(x.dims)))

# KnetArray <- KnetArray
convert(::Type{KnetArray}, x::KnetArray{T,N}) where {T,N} = x
convert(::Type{KnetArray{T}}, x::KnetArray{T,N}) where {T,N} = x
convert(::Type{KnetArray{T,N}}, x::KnetArray{T,N}) where {T,N} = x
convert(::Type{KnetArray{T}}, x::KnetArray{S,N}) where {T,N,S} = convert(KnetArray{T,N}, x)
convert(::Type{KnetArray{T,N}}, x::KnetArray{S,N}) where {T,N,S} = convert(KnetArray{T,N},copyto!(Array{S,N}(undef,x.dims), 1, x, 1, prod(x.dims)))

# KnetArray <- AbstractArray
convert(::Type{KnetArray}, x::AbstractArray{T,N}) where {T,N} = convert(KnetArray{T,N}, x)
convert(::Type{KnetArray{T}}, x::AbstractArray{S,N}) where {T,N,S} = convert(KnetArray{T,N}, x)
convert(::Type{KnetArray{T,N}}, x::AbstractArray{S,N}) where {T,N,S} = copyto!(KnetArray{T,N}(undef,size(x)), 1, convert(Array{T,N},x), 1, length(x))

# Array <- KnetArray
convert(::Type{Array}, x::KnetArray{T,N}) where {T,N} = convert(Array{T,N}, x)
convert(::Type{Array{T}}, x::KnetArray{S,N}) where {T,N,S} = convert(Array{T,N}, x)
convert(::Type{Array{T,N}}, x::KnetArray{S,N}) where {T,N,S} = convert(Array{T,N},copyto!(Array{S}(undef,x.dims), 1, x, 1, prod(x.dims)))

# Ptr <- KnetArray

unsafe_convert(::Type{Ptr{T}}, a::KnetArray) where {T} = unsafe_convert(Ptr{T}, pointer(a))
pointer(a::KnetArray{T}) where {T} = convert(Ptr{T}, a.ptr.ptr)
pointer(a::KnetArray{T},i) where {T} = convert(Ptr{T}, a.ptr.ptr + (i-1)*sizeof(T))

function unsafe_convert(T::Type{<:CuPtr}, x::KnetArray)
    T(UInt(x.ptr.ptr))
end

# Extend function CuArray to create a memory shared CuArray from KnetArray:
# Avoid the cu function as it changes eltype to Float32
function CuArray(x::KnetArray{T}) where {T}
    p = CuPtr{T}(UInt(x.ptr.ptr))
    unsafe_wrap(CuArray{T}, p, x.dims; own=false)
end

# Extend function KnetArray to create a memory shared KnetArray from CuArray:
function KnetArray(x::CuArray{T,N}) where {T,N}
    p = Base.bitcast(Cptr, pointer(x))
    k = KnetPtr(p, sizeof(x), Int(CUDA.device().handle), x)
    KnetArray{T,N}(k, size(x))
end

function ka(x...)
    @warn "ka() is deprecated, please use KnetArray instead" maxlog=1
    KnetArray(x...)
end


#!/usr/bin/env julia

using Luxor, Colors, Test, Random

Random.seed!(42)

function spirals()
    gsave()
    scale(0.3)
    r = 200
    setcolor("gray")
    for i = 0:pi/8:2pi
        gsave()
        translate(r * cos(i), r * sin(i))
        rotate(i)
        julialogo()
        grestore()
    end
    grestore()
    juliacircles(20)
end

function expandingspirals()
    gsave()
    scale(0.3)
    r = 200
    for i = pi:pi/12:6pi
        gsave()
        translate(i / 3 * r * cos(i), i / 3 * r * sin(i))
        scale(0.8)
        rotate(i)
        julialogo()
        grestore()
    end
    grestore()
end

function dropshadow()
    steps = 20
    # white-gray ramp
    gramp = range(
        colorant"white",
        stop = colorant"gray60",
        length = steps,
    )
    gsave()
    r = 200
    sethue("purple")
    rect(O, 5, 10, :stroke)
    setopacity(0.1)
    for i = 1:steps
        translate(-0.6, -0.5)
        julialogo(bodycolor = gramp[i], color = false)
    end
    julialogo()
    grestore()
end

function colorgrid()
    #cols = colormap("RdBu", 5; mid=0.5, logscale=false)
    #cols = sequential_palette(rand(10:360), 5, b=0.1)
    cols = distinguishable_colors(25)
    gsave()

    pagetiles = Tiler(500, 400, 5, 5)
    for (pos, n) in pagetiles
        gsave()
        setcolor(color(cols[n]))
        translate(pos)
        scale(0.3)
        julialogo(color = false)
        grestore()
    end
    grestore()
end

function boxes_and_rectangles(pt::Point)
    for i = 1:20
        randomhue()
        poly(
            box(pt + i, 10i, 10i, vertices = true),
            :stroke,
            close = true,
        )
    end
    for i = 40:10:150
        randomhue()
        translate(i, 0)
        poly(squircle(O, 30, 30, vertices = true), :stroke)
    end
end

function draw_julia_logos(fname)
    Drawing(1600, 1600, fname)
    origin()
    background("white")

    translate(-500, -200)
    spirals()

    translate(750, 0)
    expandingspirals()

    translate(-1000, 500)
    dropshadow()

    translate(800, 50)
    colorgrid()

    translate(-700, 300)
    boxes_and_rectangles(O)

    @test finish() == true
    println("...finished test: output in $(fname)")
end

# requires Luxor >= 2.0
function julialogodims(fname)
    Drawing(1600, 1600, fname)
    origin()
    background("white")
    sethue("black")
    julialogo(action = :path, centered = true)
    p = pathtopoly()
    lox, hix =
        extrema(first.(collect(Iterators.flatten(p))))
    loy, hiy = extrema(getfield.(
        collect(Iterators.flatten(p)),
        :y,
    ))
    bbx = BoundingBox(Point(lox, loy), Point(hix, hiy))
    sethue("grey58")
    box(bbx, :stroke)
    rule(boxmiddlecenter(bbx), boundingbox = bbx)
    rule(boxmiddlecenter(bbx), π / 2, boundingbox = bbx)
    dimension(
        boxtopleft(bbx),
        format = (d) -> string(round(d)),
        boxtopright(bbx),
        offset = -50,
        textgap = 0,
        textrotation = -π / 2,
        texthorizontaloffset = 20,
    )
    dimension(
        boxtopleft(bbx),
        format = (d) -> string(round(d)),
        boxbottomleft(bbx),
        textrotation = π,
        offset = 30,
        texthorizontaloffset = 20,
    )
    julialogo(centered = true)
    @test finish() == true
    println("...finished test: output in $(fname)")
end

function test1()
    randomhue()
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(action = :path, centered = true)
        strokepath()
    end

    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(action = :path, centered = true)
        strokepath()
    end
    text(
        "action=:path, centered=true)",
        halign = :center,
        Point(0, 100),
    )
end

function test2()
    randomhue()
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(action = :clip, centered = true)
        paint()
        clipreset()
    end

    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(action = :clip, centered = true)
        paint()
        clipreset()
    end

    text(
        "action=:clip, centered=true",
        halign = :center,
        Point(0, 100),
    )
end

function test3()
    sethue("blue")
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(action = :stroke, centered = true)
    end
    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(action = :stroke, centered = true)
    end
    text(
        "OG> action=:stroke, centered=true <NEW",
        halign = :center,
        Point(0, 100),
    )
end

function test4()
    randomhue()
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            color = true,
            bodycolor = "purple",
        )
    end

    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            color = true,
            bodycolor = "purple",
        )
    end

    text(
        "OG> centered=true, bodycolor=\"purple\" <NEW",
        halign = :center,
        Point(0, 100),
    )
end

function test5()
    randomhue()
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            color = true,
            bodycolor = "purple",
        )
    end

    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            color = true,
            bodycolor = "purple",
        )
    end

    text(
        "centered=true, color=true, bodycolor=\"purple\"",
        halign = :center,
        Point(0, 100),
    )
end

function test6()
    randomhue()
    @layer begin
        translate(-100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            action = :path,
            color = true,
        )
    end
    fillpath()
    @layer begin
        translate(100, 0)
        scale(0.5)
        julialogo(
            centered = true,
            action = :path,
            color = true,
        )
    end
    fillpath()

    text(
        "centered=true, color=true, bodycolor=\"purple\"",
        halign = :center,
        Point(0, 100),
    )
end

function draw_julia_logos(fname)
    Drawing(1600, 1600, fname)
    origin()
    background("white")
    fontsize(12)
    tiles = Tiler(1200, 1200, 3, 2)
    for (pos, n) in tiles
        @layer begin
            translate(pos)
            n == 1 && test1()
            n == 2 && test2()
            n == 3 && test3()
            n == 4 && test4()
            n == 5 && test5()
            n == 6 && test6()
        end
    end
    @test finish() == true
    println("...finished test: output in $(fname)")
    finish()
end

draw_julia_logos("julia-logo-draw.png")
julialogodims("julia-logo-dims.png")
draw_julia_logos("julia-logo-draw-table.png")


## This function prints some information on what should be the contents the user has to give in the different sections of the package.
# To select from which section you want the information from, just type the keywords:
#        Model, Simulation/s, Pseudo-Data/Pseudodata, MLE, Inference/Stan

function infoAll(woo=[])

    if typeof(woo) != String
        println("Sorry, but the only allowed input to this function is a string :(");

        print(string("

Please, if you want information about one of the package structures, type:
    1) ","\"","model","\""," for the model generation section
    2) ","\"","simulation","\"",", ","\"","simulation","\""," or ","\"","simul","\""," for the model simulation section
    3) ","\"","pseudo-data","\"",", ","\"","pseudodata","\"",", ","\"","pseudo data","\""," for the model pseudo-data generation section
    4) ","\"","mle","\"",", ","\"","likelihood","\""," for the maximum likelihood estimation section
    5) ","\"","inference","\"",", ","\"","stan","\"",", ","\"","stan inference","\"",", ","\"","staninference","\""," for the Bayesian parameter inference section
    5b) ","\"","turing","\"",", ","\"","turing inference","\"",", ","\"","turinginference","\""," for the Bayesian parameter inference section using Turing.jl
    6) ","\"","oedms","\"",", ","\"","model selection","\"",", ","\"","modelselection","\"",", ","\"","oed model selection","\""," for the optimal experimental design for model selection section
    7) ","\"","oedmc","\"",", ","\"","model calibration","\"",", ","\"","modelcalibration","\"",", ","\"","oed model calibration","\""," for the optimal experimental design for model calibration section

        "))

        return
    end

    woohoo = lowercase(woo);

    if woohoo == "model"
        print(string(raw"

                CALL defModStruct()

model_def[","\"","NameF","\"","] = [];
        # String containing the name of the model. Scripts and results will be stored using this name

model_def[","\"","nStat","\"","] = [];
        # Integer indicating the total number of steps of the model.

model_def[","\"","nPar","\"","] = [];
        # Integer indicating the total number of parameters of the model.

model_def[","\"","nInp","\"","] = [];
        # Integer indicating the total number of stimuli (inducers) of the model. If the model has no inputs, set it to 0.

model_def[","\"","stName","\"","] = [];
        # Vector of strings indicating the name of all the states of the model (without a d letter in front).

model_def[","\"","parName","\"","] = [];
        # Vector of strings indicating the name of all the parameters of the model.

model_def[","\"","inpName","\"","] = [];
        # Vector of strings indicating the name of all the stimuli (inducers) of the model. If the model has no inputs
        # just give an empty vector.

model_def[","\"","eqns","\"","] = [];
        # Vector of strings containing all the equations for the model (left and right-hand sides).
        # If an equation represents a state, the left-hand side has to be one of the strings contained in
        # stName but with a d in front (example: Prot -> dProt = ...).
        # Equations that are not states of ODEs are also allowed. Same as Julia expressions (println, for, if, etc.)
        # If you want to include a condition (if) for a state variable, each one of the if statement has to be
        # written in a separate string. Be careful with this, since then your Stan model will not work. To make it
        # work you have to write it in Stan language, but then the Julia code will not work. We recommend that if so,
        # first generate the stan code and then apply all the necessary modifications there. However, if you do not care
        # for the Julia code and just want the Stan code go on. Just know that in this case, if there is a condition in
        # one of the states, in stan you need to type the full if statement in one same string.
        # Note that models without external inputs are also supported except for the optimal experimental design sections.

model_def[","\"","Y0eqs","\"","] = [];
        # Vector of strings containing the steady-state equations of the model if desired (if not, just leave
        # it as an empty vector). These equations will be used to compute y0 assuming steady-state reached
        # before the experiment.
        # All the states must appear (identified in the left-hand side, but this time without the d in front),
        # however other equations are allowed.
        # If some  element of the equation requires an experimental value for the calculation,
        # please add exp at the beginning of the state (example: Cmrna -> expCmrna).
        # Please, do not use the name alp for anything, since this is reserved.

model_def[","\"","Y0Sim","\"","] = [];
        # If analytical solution of the model at steady state is not accurate enough and you want to add
        # an Over-Night simulation before the actual experiment simulation.
        # Allowed vales are true, false, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\"",".
        # Default value is false.
        # Time-scale for the simulation is assumed in minutes (1440 min). If this wants to be changed, what should be
        # introduced here is a number for the time conversion (e.g. 1/60 if to convert to days or 60 if to convert to seconds)

model_def[","\"","tols","\"","] = [];
        # Vector of 2 floats containing the relative and absolute tolerances for the solver (in this order).
        # If left empty, 1e-6 will be assumed for both.

model_def[","\"","solver","\"","] = [];
        # IVP solver to solve the ODEs. If nothing specified, the default will be Tsit5().
        # For more info check https://diffeq.sciml.ai/v2.0/tutorials/ode_example.html

"));

    elseif woohoo == "simulation" || woohoo == "simulations" || woohoo == "simul"
        print(string(raw"

                CALL defSimulStruct()
        MAIN STRUCTURE

simul_def[","\"","Nexp","\"","] = [];
        # Integer indicating the number of experiments to be simulated

simul_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

simul_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

simul_def[","\"","y0","\"","] = [];
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

simul_def[","\"","preInd","\"","] = [];
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

simul_def[","\"","uInd","\"","] = [];
        # Array containing the values for the stimuli at each step for each experiment. In each sub-array, columns
        # indicate a step and rows indicate an inducer (if multiple ones are considered).

simul_def[","\"","theta","\"","] = [];
        # Vector/Matrix with the parameter samples or directory and file location of CSV file with them.

simul_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

simul_def[","\"","plot","\"","] = [];
        # Boolean or yes/no string to save the resulting simulations in the results directory (false will be
        # considered as default).

simul_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated scripts and result files so they are not overwritten.
        # If empty, nothing will be added.

"))

print(string(raw"

            CALL defSimulStructFiles()

        STRUCTURE IF CSV FILES ARE USED

simul_def[","\"","ObservablesFile","\"","] = [];
        # Vector of strings containing the name of the files that have the information about the sampling
        # times and y0 values.

simul_def[","\"","EventInputsFile","\"","] = [];
        # Vector of strings containing the name of all the files that have the information about the stimuli
        # and events of the experiment.

simul_def[","\"","theta","\"","] = [];
        # Vector/Matrix with the parameter samples or directory and file location of CSV file with them.

simul_def[","\"","MainDir","\"","] = [];
        # Main directory path for the files in ObervablesFile and EventInputsFile.

simul_def[","\"","plot","\"","] = [];
        # Boolean or yes/no string to save the resulting simulations in the results directory (false will be
        # considered as default).

simul_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated scripts and result files so they are not overwritten.
        # If empty, nothing will be added.

"))

    elseif woohoo == "pseudo-data" || woohoo == "pseudodata" || woohoo == "pseudo data"
        print(string(raw"

            CALL defPseudoDatStruct()

        MAIN STRUCTURE

pseudo_def[","\"","Nexp","\"","] = [];
        # Integer indicating the number of experiments to be simulated

pseudo_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

pseudo_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

pseudo_def[","\"","y0","\"","] = [];
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

pseudo_def[","\"","preInd","\"","] = [];
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

pseudo_def[","\"","uInd","\"","] = [];
        # Array containing the values for the stimuli at each step for each experiment. In each sub-array, columns
        # indicate a step and rows indicate an inducer (if multiple ones are considered).

pseudo_def[","\"","theta","\"","] = [];
        # Vector/Matrix with the parameter samples or directory and file location of CSV file with them.

pseudo_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

pseudo_def[","\"","plot","\"","] = [];
        # Boolean or yes/no string to save the resulting simulations in the results directory (false will be
        # considered as default).

pseudo_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated scripts and result files so they are not overwritten.
        # If empty, nothing will be added.

pseudo_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is either a vector of strings, a vector of integers
        # indicating which entries from model_def[","\"","stName","\"","] are observables. If a vector of
        # strings is given, these could also be an expression combining states (Only +,-,*,/ and ^ will be
        # considered).

pseudo_def[","\"","NoiseType","\"","] = [];
        # String indicating if the desired noise is homoscedastic or heteroscedastic. The allowed strings are
        # homo, homoscedastic or hetero, heteroscedastic (case independent). If empty, default will be heteroscedastic.
        # Note that for high homoscedastic noise data could become negative (which to me does not make much sense), for
        # now this will be set to a value close to 0.

pseudo_def[","\"","Noise","\"","] = [];
        # Percentage of heteroscedastic noise (introduced as value from 0 to 1). If empty 10% heteroscedastic
        # noise will be assumed. In the homoscedastic case, if empty a standard deviation of 1 will be assumed.
        # This has to be a vector of noise values for each observable.

"))

        print(string(raw"

            CALL defPseudoDatStructFiles()

        STRUCTURE IF CSV FILES ARE USED

pseudo_def[","\"","ObservablesFile","\"","] = [];
        # Vector of strings containing the name of the files that have the information about the sampling
        # times and y0 values.

pseudo_def[","\"","EventInputsFile","\"","] = [];
        # Vector of strings containing the name of all the files that have the information about the stimuli
        # and events of the experiment.

pseudo_def[","\"","theta","\"","] = [];
        # Vector/Matrix with the parameter samples or directory and file location of CSV file with them.

pseudo_def[","\"","MainDir","\"","] = [];
        # Main directory path for the files in ObervablesFile and EventInputsFile.

pseudo_def[","\"","plot","\"","] = [];
        # Boolean or yes/no string to save the resulting simulations in the results directory (false will be
        # considered as default).

pseudo_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated scripts and result files so they are not overwritten.
        # If empty, nothing will be added.

pseudo_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is either a vector of strings, a vector of integers
        # indicating which entries from model_def[","\"","stName","\"","] are observables. If a vector of
        # strings is given, these could also be an expression combining states (Only +,-,*,/ and ^ will be
        # considered).

pseudo_def[","\"","NoiseType","\"","] = [];
        # String indicating if the desired noise is homoscedastic or heteroscedastic. The allowed strings are
        # homo, homoscedastic or hetero, heteroscedastic (case independent). If empty, default will be heteroscedastic.
        # Note that for high homoscedastic noise data could become negative (which to me does not make much sense), for
        # now this will be set to a value close to 0.

pseudo_def[","\"","Noise","\"","] = [];
        # Percentage of heteroscedastic noise (introduced as value from 0 to 1). If empty 10% heteroscedastic
        # noise will be assumed. In the homoscedastic case, if empty a standard deviation of 1 will be assumed.
        # This has to be a vector of noise values for each observable.

"))

    elseif woohoo == "mle" || woohoo == "likelihood"
        print(string(raw"

                CALL defMLEStruct()

mle_def[","\"","Nexp","\"","] = [];
        # Integer indicating the number of experiments to be simulated

mle_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

mle_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

mle_def[","\"","y0","\"","] = [];
        # Array (single simulation) of Y0s for the simulations for the
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

mle_def[","\"","preInd","\"","] = [];
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

mle_def[","\"","uInd","\"","] = [];
        # Array containing the values for the stimuli at each step for each experiment. In each sub-array, columns
        # indicate a step and rows indicate an inducer (if multiple ones are considered).

mle_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

mle_def[","\"","plot","\"","] = [];
        # Boolean or yes/no string to save the resulting simulations in the results directory (false will be
        # considered as default).

mle_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated scripts and result files so they are not overwritten.
        # If empty, nothing will be added.

mle_def[","\"","thetaMAX","\"","] = [];
        # Vector containing the maximum bounds for theta (no files can be introduced)

mle_def[","\"","thetaMIN","\"","] = [];
        # Vector containing the minimum bounds for theta (no files can be introduced)

mle_def[","\"","runs","\"","] = [];
        # Integer indicating how many runs of Optimisation will be done. You will get as many theta vectors
        # as runs selected.

mle_def[","\"","parallel","\"","] = [];
        # Boolean or yes/no string indicating if the different runs want to be done in parallel (true) or
        # series (false). Default is false.

    # For the two following fields, you can introduce a string pointing to the observable files (same strings in)
    #     both fields having the same structure as the ones generated in the PseudoData section. If multiple theta are
    #     considered in the file, then the covariance matrix will be taken.
mle_def[","\"","DataMean","\"","] = [];
        # Array containing the vector of means for each experiment.

mle_def[","\"","DataError","\"","] = [];
        # Array containing the vector or matrices (covariance included) of errors for the data for each experiment.

                                        IMPORTANT!!!!

        # Whilst each entry (experiment) of DataMean can be a matrix where each column is an observable of the
        # system, for DataError this is not the case. Each entry of the array (experiment) will have as many
        # entries as observables, where it would be a vector of errors or a matrix. This is done this way to
        # generalise the presence of both options.

mle_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is either a vector of strings, a vector of integers
        # indicating which entries from model_def[","\"","stName","\"","] are observables. If a vector of
        # strings is given, these could also be an expression combining states (Only +,-,*,/ and ^ will be
        # considered).

mle_def[","\"","OPTsolver","\"","] = [];
        # For now we only use the package BlackBoxOptim, so any of their options can be used. The default
        # is adaptive_de_rand_1_bin_radiuslimited. Please, introduce it as a string.

mle_def[","\"","MaxTime","\"","] = [];
        # Integer indicating the maximum number of time allowed for the optimisation as a stop criterion.
        # If this is selected MaxFuncEvals has to be empty (only 1 stop criterion). If both are empty,
        # the default will be to do 1000 function evaluations

mle_def[","\"","MaxFuncEvals","\"","] = [];
        # Integer indicating the maximum number of function evaluations allowed for the optimisation as a
        # stop criterion. If this is selected MaxTime has to be empty (only 1 stop criterion). If both are
        # empty, the default will be to do 1000 function evaluations

"))

    elseif woohoo == "inference" || woohoo == "stan" || woohoo == "stan inference" || woohoo == "staninference"
        print(string(raw"

            CALL defBayInfStruct()

        MAIN STRUCTURE

bayinf_def[","\"","Priors","\"","] = []; # Five options for this:
        # 1) A 2*N array containing the bounds for the parameters. Order of parameters will be assumed as
            # the one introduced in model_def[","\"","parName","\"","]. As a prior a truncated Normal covering 2
            # standard deviations in the bounds given will be generated as prior.
        # 2) Path to a CSV file containing samples for the parameters. Fitting of the samples to
            # different type of distributions will be done to generate the priors. Order of parameters
            # will be assumed as the one introduced in model_def[","\"","parName","\"","]. You can also
            # introduce a 2D array of floats with the samples (Array{Float64,2}).
        # 3) Dictionary with fields pars, transpars and pridis defining the
            # parameters, subsequent desired transformations and prior distributions.
            # In the field pars, parameters
            # have to be defined with the same names and order as in model_def[","\"","parName","\"","],
            # otherwise the script will not proceed.
        # 4) An empty array. If this is the case, the stan model will be generated with nothing in
            # the parameters and transformed parameters section. The path to the stan file will
            # be given so the user can fill these sections.
        # 5) Path to a Stan model file if you already have one. For example, if first you introduce an
            # empty array, then you can fill the parameters section and run from that script instead
            # of having to copy the things in here.

bayinf_def[","\"","Data","\"","] = []; # Two options:
        # 1) A dictionary containing 3 fields. Observables, for the path to the files containing the
            # experimental data. Inputs, for the path to the files containing the input profiles for the
            # experiments. Obs, a string vector containing the observables of the experiments.
            # The format of the files has to be the same one as the ones generated in the
            # pseudo-data section. For each of the 2 entries, more than one file can be given if a
            # multi-experimental inference wants to be done. Obs file also needs to be given.
            # A y0 in a field with the same name has to be given for each experiment. The field is
            # compulsory, even if not used.
            # WARNING: For now, this option does not include extraction of covariance matrix for data.
        # 2) A dictionary containing the same structure as the simul_def plus the fields DataMean and
            # DataError containing the experimental data. You can call the function defBayInfDataStruct()
            # to obtain the empty structure of the dictionary.

bayinf_def[","\"","StanSettings","\"","] = []; # Two options:
        # 1) A dictionary with the basic fields from a stan run. The structure of the dictionary can be
            # extracted calling the function defBasicStanSettingsStruct().
        # 2) An empty array. If this is the case, no inference will be done after calling the main
            # function. Instead, you will be given a StanModel file and data structure and you will
            # have to use this to run a call of Stan by yourself. An example will be provided.

bayinf_def[","\"","flag","\"","] = [];
            # String to attach a unique flag to the generated files so it is not overwritten.
            # If empty, nothing will be added.

bayinf_def[","\"","plot","\"","] = [];
            # true, flase, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\""," or []
            # indicating if plots with the results will be generated. Default is false.

bayinf_def[","\"","runInf","\"","] = [];
            # true, flase, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\""," or []
            # indicating if inference wants to be performed or only stan structure is given.
            # Default is true (if all the necessary elements are present)

bayinf_def[","\"","MultiNormFit","\"","] = [];
            # true, false, ","\"","yes","\"",", ","\"","no","\"",", ","\"","Yes","\"",", ","\"","No","\""," or []
            # indicating that if samples are given to fit, the prior will be fitted as a MultiNormal distribution
            # or not. the default is false. If some parameter is better fit with a Log-Normal distribution, this
            # will be used with a distribution reparameterisation to begin to be included in the multinormal.
            # If for some parameter a Uniform distribution is better, this parameter will be excluded from
            # the multinormal and defined as a separate parameter. This can be used as an example of how to set
            # your Stan Model to use multi_normal priors.

"))

print(string(raw"

            CALL defBayInfDataStruct()

        DATA DICTIONARY STRUCTURE

data_def[","\"","Nexp","\"","] = [];
        # Integer indicating the number of experiments to be simulated

data_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

data_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

data_def[","\"","y0","\"","] = [];
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

data_def[","\"","preInd","\"","] = [];
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

data_def[","\"","uInd","\"","] = [];
        # Array containing the values for the stimuli at each step for each experiment. In each sub-array, columns
        # indicate a step and rows indicate an inducer (if multiple ones are considered).

data_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

data_def[","\"","DataMean","\"","] = [];
        # Array containing the vector of means for each experiment.

data_def[","\"","DataError","\"","] = [];
        # Array containing the vector or matrices (covariance included) of errors for the data for each experiment.

                                        IMPORTANT!!!!

        # Whilst each entry (experiment) of DataMean can be a matrix where each column is an observable of the
        # system, for DataError this is not the case. Each entry of the array (experiment) will have as many
        # entries as observables, where it would be a vector of errors or a matrix. This is done this way to
        # generalise the presence of both options.

data_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is either a vector of strings, a vector of integers
        # indicating which entries from model_def[","\"","stName","\"","] are observables. If a vector of
        # strings is given, these could also be an expression combining states (Only +,-,*,/ and ^ will be
        # considered).

"))

print(string(raw"

            CALL defBayInfDataFromFilesStruct()

        DATA DICTIONARY STRUCTURE IF FILES ARE GIVEN

data_def[","\"","Observables","\"","] = [];
        # Vector of strings containing the name of the files that have the information about the sampling
        # times and y0 values.

data_def[","\"","Inputs","\"","] = [];
        # Vector of strings containing the name of all the files that have the information about the stimuli
        # and events of the experiment.

data_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is either a vector of strings, a vector of integers
        # indicating which entries from model_def[","\"","stName","\"","] are observables. If a vector of
        # strings is given, these could also be an expression combining states (Only +,-,*,/ and ^ will be
        # considered).

data_def[","\"","y0","\"","] = [];
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

"))

print(string(raw"

            CALL defBasicStanSettingsStruct()

        STAN SETTINGS DICTIONARY STRUCTURE

stan_def[","\"","cmdstan_home","\"","] = [];
        # String containing the full path to the installation directory for cmdstan

stan_def[","\"","nchains","\"","] = [];
        # Number of chains for the inference (integer)

stan_def[","\"","nsamples","\"","] = [];
        # Number of post-warmup samples for each chain (integer)

stan_def[","\"","nwarmup","\"","] = [];
        # Number of warm-up samples for each chain (integer)

stan_def[","\"","printsummary","\"","] = [];
        # Print sumary of inference at the end. This can be either true or false. Default will be true.

stan_def[","\"","init","\"","] = [];
        # Initial point for the parameters in each chain. See output of MLE results. This field can be empty.
        # Please introduce the parameter values in their true parameter range.

stan_def[","\"","maxdepth","\"","] = [];
        #  Maximum tree-depth. This field can be empty. Check Stan documentation for more information.

stan_def[","\"","adaptdelta","\"","] = [];
        # Delta value between 0 and 1. This field can be empty.  Check Stan documentation for more information.

stan_def[","\"","jitter","\"","] = [];
        # Jitter value between 0 and 1. This field can be empty.  Check Stan documentation for more information.

"));

elseif woohoo == "turing" || woohoo == "turinginference"
    print(string(raw"

                CALL defTurInfStruct()

            MAIN STRUCTURE

    turinf_def[","\"","Priors","\"","] = []; # Three options for this:
            # 1) A 2*N array containing the bounds for the parameters. Order of parameters will be assumed as
                # the one introduced in model_def[","\"","parName","\"","]. As a prior a truncated Normal covering 2
                # standard deviations in the bounds given will be generated as prior.
            # 2) Path to a CSV file containing samples for the parameters. Fitting of the samples to
                # different type of distributions will be done to generate the priors. Order of parameters
                # will be assumed as the one introduced in model_def[","\"","parName","\"","]. You can also
                # introduce a 2D array of floats with the samples (Array{Float64,2}).
            # 3) Array containing any distribution from the Julia package Distributions.jl with their
                # corresponding parameters. Order of parameters will be assumed as
                # the one introduced in model_def[","\"","parName","\"","].

    turinf_def[","\"","Data","\"","] = []; # Two options:
            # 1) A dictionary containing 3 fields. Observables, for the path to the files containing the
                # experimental data. Inputs, for the path to the files containing the input profiles for the
                # experiments. Obs, a string vector containing the observables of the experiments.
                # The format of the files has to be the same one as the ones generated in the
                # pseudo-data section. For each of the 2 entries, more than one file can be given if a
                # multi-experimental inference wants to be done. Obs file also needs to be given.
                # A y0 in a field with the same name has to be given for each experiment. The field is
                # compulsory, even if not used.
                # WARNING: For now, this option does not include extraction of covariance matrix for data.
            # 2) A dictionary containing the same structure as the simul_def plus the fields DataMean and
                # DataError containing the experimental data. You can call the function defBayInfDataStruct()
                # to obtain the empty structure of the dictionary.

    turinf_def[","\"","flag","\"","] = [];
                # String to attach a unique flag to the generated files so it is not overwritten.
                # If empty, nothing will be added.

    turinf_def[","\"","plot","\"","] = [];
                # true, flase, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\""," or []
                # indicating if plots with the results will be generated. Default is false.

    turinf_def[","\"","MultiNormFit","\"","] = [];
                # true, false, ","\"","yes","\"",", ","\"","no","\"",", ","\"","Yes","\"",", ","\"","No","\""," or []
                # indicating that if samples are given to fit, the prior will be fitted as a set of Normal distributions
                # or instead the best type of distributions that can be fit with the samples.

    turinf_def[","\"","Trunc","\"","] = [];
                # true, false, ","\"","yes","\"",", ","\"","no","\"",", ","\"","Yes","\"",", ","\"","No","\"",", [] or
                # 2*N array indicating upper and lower bounds for the priors. If true then the truncations will be
                # the smaller sample minus 10% its value (never allowing this go bellow 0) and the bigest sample plus
                # 10% its value. If false no truncation will be applaied to any prior. In all cases, order of parameters
                # will be assumed as the one introduced in model_def[","\"","parName","\"","]. Default will be true.

    "))

    elseif woohoo == "oedms" || woohoo == "model selection" || woohoo == "modelselection" || replace("woohoo", " "=>"") == "oedmodelselection"
print(string(raw"

                CALL defODEModelSelectStruct()

oedms_def[","\"","Model_1","\"","] = [];
        # Model structure for Model 1. Dict. See Model Generation Section.
        # Note that the order of the inputs is the one defined in this model. If there is a chance that
        # Model_2 has an input that does not exist in this model, these will be appended at the end
        # of the ones in Model_1.

oedms_def[","\"","Model_2","\"","] = [];
        # Model structure for Model 2. Dict. See Model Generation Section.

oedms_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is a vector of strings. These could also be an
        # expression combining states (Only +,-,*,/ and ^ will be considered).
        # Note that due to the use of the covariance matrix in some computations an addition of a small
        # number (0.1) is done in the diagonal elements (for now) to ensure this to be positive definite.
        # If the observable(s) of your model are normalised or in a range between 0 and 1 we recommend to
        # re-scale this to a larger range (0 to 100 for example) so the variances considered are as close
        # as possible to the real ones. This can be done by just multiplying the observable by a constant
        # (Obs*100 for example) in each entry of the vector.

oedms_def[","\"","Theta_M1","\"","] = [];
        # Theta vector (frequentist OED or model with 1 parameter) or matrix (Bayesian OED) for model 1.
        # Path to file with samples is also allowed.

oedms_def[","\"","Theta_M2","\"","] = [];
        # Theta vector (frequentist OED or model with 2 parameter) or matrix (Bayesian OED) for model 2.
        # Path to file with samples is also allowed.

oedms_def[","\"","y0_M1","\"","] = [];
        # For Model 1:
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

oedms_def[","\"","y0_M2","\"","] = [];
        # For Model 2:
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

oedms_def[","\"","preInd_M1","\"","] = [];
        # For Model 1:
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

oedms_def[","\"","preInd_M2","\"","] = [];
        # For Model 2:
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

oedms_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

oedms_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

oedms_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

oedms_def[","\"","fixedInp","\"","] = [];
        # If more than 1 inducer for the system exists, but only 1 input can be dynamic, give a vector
        # of strings indicating which inputs are going to be optimised but as a constant input instead
        # of dynamic. If none, just give an empty vector.
        # If there is only 1 stimuli in the model, this field will be ignored.

oedms_def[","\"","fixedStep","\"","] = [];
        # If you want any of the steps to be fixed to a value. This has to be an empty array if none is fixed
        # or an array of tuples, where each tuple is a step to be fixed. The first entry of the tuple is the
        # index of the step (as an Integer), and the second and array of values for each inducer. Note that
        # the fixed inputs will be ignored, so do not take them into account here.
        # The example type should be Array{Array{Int,1},1} or an empty array ([]) if not used.

oedms_def[","\"","equalStep","\"","] = [];
        # If you want a series of steps to have the same optimised value (for example if you want to design a
        # pulse experiment) you can introduce inside this array different arrays with the indexes of the steps
        # that will have the same value. The values introduced in each array need to be integers.

oedms_def[","\"","plot","\"","] = [];
        # true, flase, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\""," or []
        # indicating if plots with the results will be generated. Default is false.

oedms_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated files so it is not overwritten.
        # If empty, nothing will be added.

            # The order of uUpper and uLower will be taken as the order of inducers defined in Model 1.
            # If Model 2 has an aditional input, this will be added after, so consider this when introducing
            # the bounds for them.
oedms_def[","\"","uUpper","\"","] = [];
        # Vector indicating the upper bounds for the inducers

oedms_def[","\"","uLower","\"","] = [];
        # Vector indicating the lower bounds for the inducers

oedms_def[","\"","maxiter","\"","] = [];
        # Maximum number of iterations for the Bayesian Optimisation. If nothing is introduced a default
        # of 100 iterations will be taken

oedms_def[","\"","maxtime","\"","] = [];
        # Maximum time allowed for the Bayesian Optimisation. If nothing is introduced, only the
        # maximum number of iterations will be taken into account.

"))

    elseif woohoo == "oedmc" || woohoo == "model calibration" || woohoo == "modelcalibration" || replace("woohoo", " "=>"") == "oedmodelcalibration"
print(string(raw"

                CALL defODEModelCalibrStruct()

oedmc_def[","\"","Model","\"","] = [];
        # Dict with Model. See Model Generation Section.

oedmc_def[","\"","Obs","\"","] = [];
        # States of the model that are observables. This is a vector of strings.These could also be an
        # expression combining states (Only +,-,*,/ and ^ will be considered).

oedmc_def[","\"","Theta","\"","] = [];
        # Theta matrix (Bayesian OED) for the model. No single vectors will be allowed.
        # Path to file is also allowed.

oedmc_def[","\"","y0","\"","] = [];
        # Array (single simulation) or matrix (multiple simulations) of Y0s for the simulations for each
        # experiment. If you are computing the steady-state this vector might not be used, however, you
        # still need to introduce it with some random numbers.

oedmc_def[","\"","preInd","\"","] = [];
        # Vector of numbers with the values for the stimuli (inducer) in the over-night. It might be the case
        # that this entry is not required since only the y0 vector is considered for the initial point of
        # the simulation. However, you still need to introduce a random value for it to avoid future issues.

oedmc_def[","\"","finalTime","\"","] = [];
        # Vector of final times for each simulation (initial time will always be assumed as 0,
        # so please consider that).

oedmc_def[","\"","switchT","\"","] = [];
        # Array with the switching times of the inducer in the simulation (time 0 and final
        # time need to be considered)

oedmc_def[","\"","tsamps","\"","] = [];
        # Array of sampling time vectors for the experiments.

oedmc_def[","\"","fixedInp","\"","] = [];
        # If more than 1 inducer for the system exists, but only 1 input can be dynamic, give a vector
        # of strings indicating which inputs are going to be optimised but as a constant input instead
        # of dynamic. If none, just give an empty vector.
        # If there is only 1 stimuli in the model, this field will be ignored.

oedmc_def[","\"","fixedStep","\"","] = [];
        # If you want any of the steps to be fixed to a value. This has to be an empty array if none is fixed
        # or an array of tuples, where each tuple is a step to be fixed. The first entry of the tuple is the
        # index of the step (as an Integer), and the second and array of values for each inducer. Note that
        # the fixed inputs will be ignored, so do not take them into account here.

oedmc_def[","\"","equalStep","\"","] = [];
        # If you want a series of steps to have the same optimised value (for example if you want to design a
        # pulse experiment) you can introduce inside this array different arrays with the indexes of the steps
        # that will have the same value. The values introduced in each array need to be integers.

oedmc_def[","\"","plot","\"","] = [];
        # true, flase, ","\"","Yes","\"",", ","\"","yes","\"",", ","\"","No","\"",", ","\"","no","\""," or []
        # indicating if plots with the results will be generated. Default is false.

oedmc_def[","\"","flag","\"","] = [];
        # String to attach a unique flag to the generated files so it is not overwritten.
        # If empty, nothing will be added.

oedmc_def[","\"","uUpper","\"","] = [];
        # Vector indicating the upper bounds for the inducers

oedmc_def[","\"","uLower","\"","] = [];
        # Vector indicating the lower bounds for the inducers

oedmc_def[","\"","maxiter","\"","] = [];
        # Maximum number of iterations for the Bayesian Optimisation. If nothing is introduced a default
        # of 100 iterations will be taken

oedmc_def[","\"","maxtime","\"","] = [];
        # Maximum time allowed for the Bayesian Optimisation. If nothing is introduced, only the
        # maximum number of iterations will be taken into account.

oedmc_def[","\"","util","\"","] = [];
        # String indicating entropy or perc (or percentile) as the core of the utility function to
        # compute the uncertainty of the model simulations. The default will be to use percentiles.

"))

    else
        print(string("

Please, if you want information about one of the package structures, type:
    1) ","\"","model","\""," for the model generation section
    2) ","\"","simulation","\"",", ","\"","simulation","\""," or ","\"","simul","\""," for the model simulation section
    3) ","\"","pseudo-data","\"",", ","\"","pseudodata","\"",", ","\"","pseudo data","\""," for the model pseudo-data generation section
    4) ","\"","mle","\"",", ","\"","likelihood","\""," for the maximum likelihood estimation section
    5) ","\"","inference","\"",", ","\"","stan","\"",", ","\"","stan inference","\"",", ","\"","staninference","\""," for the Bayesian parameter inference section
    6) ","\"","oedms","\"",", ","\"","model selection","\"",", ","\"","modelselection","\"",", ","\"","oed model selection","\""," for the optimal experimental design for model selection section
    7) ","\"","oedmc","\"",", ","\"","model calibration","\"",", ","\"","modelcalibration","\"",", ","\"","oed model calibration","\""," for the optimal experimental design for model calibration section

        "))
    end

end


# # Basic Tutorial

# Since DFControl is aimed at improving the day to day quality of life of a material's scientist/anyone running DFT codes,
# we will look at a simple demonstration of how by creating and submitting some
# [Quantum-Espresso](https://www.quantum-espresso.org/) calculations on Si
# starting from a cif file specifying the structure.

using DFControl

# First we download the cif file, extract the `Structure` and assign the right pseudos to it.
# In this case Si (F d -3 m :1) from http://www.crystallography.net/cod/9011998.cif
using Downloads
cif_file = Downloads.download("http://www.crystallography.net/cod/9011998.cif", "Si.cif")

structure = Structure(cif_file; name = "Si")
set_pseudos!(structure, :pbesol)

# This assumes that the `:pbesol` pseudopotential set was installed during the
#md # [configuration step](@ref Configuration).

# Next we specify the executables with which to run the QE calculations.
# We assume here that `mpirun` is installed and in the user's PATH, and that
# QE is installed, and to be found in the `/opt/qe/bin`, change this according to your own
# setup.

pw_execs = [Exec("mpirun", "", :np => 4), Exec("pw.x", "/opt/qe/bin/", :nk => 4)]

# Additional executable flags can be passed as varargs to the constructor of `Exec`,
# e.g. `Exec("pw.x", "/opt/qe/bin/", :nk => 4, :ndiag => 2)`.

# Then we create the first calculation for our job, we name it scf, which will be used to reference it later.
# We also pass the executables to be used and additional flags to be set to the constructor.
# Afterwards we set the kpoints to be used in the scf calculation.
scf_calculation = Calculation{QE}("scf", :calculation => "scf"; execs = pw_execs)
set_kpoints!(scf_calculation, (6, 6, 6, 1, 1, 1))

# The code recognizes internally that this 6-Tuple corresponds to a
# `K_POINTS (automatic)` block in QE. Alternatively (leading to an identical final result):

scf_calculation = Calculation{QE}("scf", :calculation => "scf"; execs = pw_execs,
                                  data = [InputData(:k_points, :automatic,
                                                    (6, 6, 6, 1, 1, 1))])

# We can now define our job:
job = DFJob("Si", structure, [scf_calculation], :ecutwfc => 20, :conv_thr => 1e-6;
            local_dir = "job")

# Additional calculations would be be added to the list `[scf_calculation]`.
# The flag => value pairs will set the specified flags to that value for all calculations in the job
# that allow recognize that flag, so it's ideal for things like cutoffs and smearing etc.

# We are now ready to submit the job, which will run in the current working directory
if false #hide
    submit(job)
else #hide
    global job = DFJob(joinpath(@__DIR__, "../../src/assets/job/"))#hide
    pop!(job) #hide
end #hide

# This will generate and save all calculation files, and the corresponding job script (`job.tt`),
# and subsequently run the job.
# First submission through `sbatch job.tt` will be tried, if that fails then the script will run
# through `bash job.tt`.

# After the job finishes the outputs can be parsed through
outputdata(job)
# or for a specific calculation
outputdata(job["scf"])

# This also demonstrates how a calculation can be referenced using its name
# (remember that we named the calculation "scf" when creating it).

# Now that the scf calculation finished succesfully, the next step is usually to
# have a look at the bandstructure. For this we generate a bands calculation,
# using the scf calculation as a template and a generated high symmetry k-point path
# with 20 kpoints per segment.

bands_calc = gencalc_bands(job["scf"], high_symmetry_kpath(job.structure, 20))

# Observe the :calculation => "bands", and automatic setting of the :verbosity => "high" flags.
# We now push! this calculation to the job queue
push!(job, bands_calc)

# However, since we know the scf succeeded there is no need to rerun it.
# To un-schedule it we do
job["scf"].run = false

# Printing the job will now highlight the scheduled calculations differently from the non-scheduled ones
job

# Seeing that all is right we submit the job again
set_localdir!(job, "job"); #hide
if false #hide
    submit(job)
else #hide
    global job = DFJob(joinpath(pathof(DFControl), "..", "..", "docs", "src", "assets",
                                "job"))#hide
end #hide

# We can access the bands through
bands = readbands(job);
# or
bands = outputdata(job["bands"])[:bands];

# They can be plotted too
fermi = readfermi(job) # = outputdata(job["scf"])[:fermi]
using Plots
plot(bands; fermi = fermi)

# Since more info (such as the structure) is available in the job,
# plotting the job leads to a richer plot
plot(job, -10, 1)

# As can be seen in the [Advanced Usage](@ref), additional information
# generated by additional calculations will be picked up by DFControl
# in order to create richer plots.


#=
This is Julia implementation of Gomoku Board by Tejan Karmali.
=#

import Base: ==, deepcopy, show
using IterTools: chain

# Cell occupants
#@enum CELL_OCC WHITE = -1 EMPTY BLACK  FILL KO UNKNOWN
WHITE, EMPTY, BLACK, FILL, KO, UNKNOWN = collect(-1:4)

# Represents "group not found" in the LibertyTracker object
MISSING_GROUP_ID = -1
#=
struct PlayerMove
  color
  move
end

# making a (x,y) coordinate for board from flattened index x
coordify(x; N = 19) = 1 + (x - 1) % N, 1 + (x - 1) ÷ N

get_first_n(x::Array{Int8, 3}, n) = size(x, 3) < n ? x : x[:, :, 1:n]
=#
mutable struct GomokuPosition <: Position
  #=
  board: an array
  n: an int representing moves played so far
  recent: a tuple of PlayerMoves, such that recent[end] is the last move.
  board_deltas: an array of shape (N, N, n) representing changes
      made to the board at each move (played move and captures).
      Should satisfy next_pos.board - next_pos.board_deltas[:, :, 0] == pos.board
  to_play: BLACK or WHITE
  =#
  env::GomokuEnv
  board::Array{Int8, 2}
  n::Int
  recent::Vector{PlayerMove}
  board_deltas::Array{Int8, 3}
  to_play::Int
  done::Bool
  winner::Int8
  function GomokuPosition(env::GomokuEnv; board = nothing, n = 0, recent = Vector{PlayerMove}(),
	board_deltas = nothing, to_play = BLACK)

    b = board != nothing ? board : deepcopy(env.EMPTY_BOARD)
    bd = board_deltas != nothing ? board_deltas : zeros(Int8, env.N, env.N, 0)
    done, winner = has_game_ended(b, env)

    new(env, b, n, recent, bd, to_play, done, winner)
  end
end

function deepcopy(pos::GomokuPosition)
  new_board = deepcopy(pos.board)
  new_recent = deepcopy(pos.recent)
  return GomokuPosition(pos.env; board = new_board, n = pos.n, recent = new_recent,
                  board_deltas = pos.board_deltas, to_play = pos.to_play)
end

function show(io::IO, pos::GomokuPosition)
  pretty_print_map = Dict{Int, String}([
            WHITE => "O",
            EMPTY => ".",
            BLACK => "X",
            FILL => "#",
        ])
  board = deepcopy(pos.board)
  N = size(board, 1)

  raw_board_contents = []
  for i = 1:N
    row = []
    for j = 1:N
      appended = length(pos.recent) != 0 && (i, j) == pos.recent[end].move ? "<" : " "
      push!(row, pretty_print_map[board[i,j]] * appended)
    end
    push!(raw_board_contents, join(row))
  end

  row_labels_fmt = [@sprintf("%2d ", i) for i = N:-1:1]
  row_labels = [string(i) for i = N:-1:1]
  annotated_board_contents = [join(r) for r in zip(row_labels_fmt, raw_board_contents, row_labels)]
  header_footer_rows = ["   " * join("ABCDEFGHJKLMNOPQRST"[1:N], " ") * "   "]
  annotated_board = join(collect(chain(header_footer_rows, annotated_board_contents,
                                        header_footer_rows)), "\n")
  details = "\nMove: $(pos.n)\n"
  turn = pos.done ? "GAME OVER\n" : "To Play: " * (pos.to_play == BLACK ? "X(BLACK)\n" : "O(WHITE)\n")
  println(annotated_board * details * turn)
end

is_move_legal(pos::GomokuPosition, move) = pos.board[move...] == EMPTY

all_legal_moves(pos::GomokuPosition) = vec(pos.board .== EMPTY)

has_game_ended(pos::GomokuPosition) = has_game_ended(pos.board, pos.env)

function has_game_ended(board::Array{Int8, 2}, env)
  n_in_row = env.n_in_row
  dim = env.N

  for h = 1:dim
    for w = 1:dim
      if w in 1:dim-n_in_row+1 && board[h,w]!=EMPTY
        pieces = Set([board[h, i] for i = w:w+n_in_row-1])
        length(pieces) == 1 && return true, collect(pieces)[1]
      end

      if h in 1:dim-n_in_row+1 && board[h,w]!=EMPTY
        pieces = Set([board[i, w] for i = h:h+n_in_row-1])
	length(pieces) == 1 && return true, collect(pieces)[1]
      end

      if w in 1:dim-n_in_row+1 && h in 1:dim-n_in_row+1 && board[h,w]!=EMPTY
        pieces = Set([board[h+i,w+i] for i=0:n_in_row-1])
        length(pieces) == 1 && return true, collect(pieces)[1]
      end

      if w in n_in_row:dim && h in 1:dim-n_in_row+1 && board[h,w]!=EMPTY
        pieces = Set([board[h+i,w-i] for i=0:n_in_row-1])
        length(pieces) == 1 && return true, collect(pieces)[1]
      end
    end
  end

  sum(board .== EMPTY) == 0 && return true, EMPTY

  false, EMPTY
end

function flip_playerturn!(pos::GomokuPosition; mutate = false)
  new_pos = mutate ? pos : deepcopy(pos)
  new_pos.to_play *= -1
  return new_pos
end

function play_move!(pos::GomokuPosition, c; color = nothing, mutate = false)
  if color == nothing
    color = pos.to_play
  end

  new_pos = mutate ? pos : deepcopy(pos)

  @assert !new_pos.done

  if !is_move_legal(pos, c)
    throw(IllegalMove())
  end

  new_pos.board[c...] = color
  opp_color = -color

  N = size(pos.board, 1)

  new_board_delta = zeros(Int8, N, N)
  new_board_delta[c...] = color

  new_pos.n += 1
  push!(new_pos.recent, PlayerMove(color, c))
  # keep a rolling history of last 7 deltas - that's all we'll need to
  # extract the last 8 board states.

  new_pos.board_deltas = cat(dims=3, reshape(new_board_delta, N, N, 1),
			     get_first_n(new_pos.board_deltas, new_pos.env.planes-2))
  new_pos.to_play *= -1
  new_pos.done, new_pos.winner = has_game_ended(new_pos)
  return new_pos
end

score(pos::GomokuPosition) = pos.winner

function result(pos::GomokuPosition)
  points = score(pos)
  if points > 0
    return 1
  elseif points < 0
    return -1
  else
    return 0
  end
end

function result_string(pos::GomokuPosition)
  points = score(pos)
  if points > 0
    return "B"
  elseif points < 0
    return "W"
  else
    return "DRAW"
  end
end

function replay_position(pos::GomokuPosition, result)
  #=
  Wrapper for a go.GoPosition which replays its history.
  Assumes an empty start position! (i.e. no handicap, and history must be exhaustive.)

  Result must be passed in, since a resign cannot be inferred from position
  history alone.

  for position_w_context in replay_position(position):
    print(position_w_context.position)
  =#
  pos.n == length(pos.recent) ? nothing : throw(AssertionError("GomokuPosition history is incomplete"))
  replay_buffer = Vector{PositionWithContext}()
  dummy_pos = GomokuPosition(pos.env)

  for player_move in pos.recent
    color, next_move = player_move.color, player_move.move
    push!(replay_buffer, PositionWithContext(dummy_pos, next_move, result))
    dummy_pos = play_move!(dummy_pos, next_move, color = color)
  end
  return replay_buffer
end


module AtmosphericStates
using CUDA
using ..Device: array_type, array_device, CUDADevice, CPU
using DocStringExtensions
using Adapt
using GaussQuadrature
using CLIMAParameters
using CLIMAParameters.Planet: grav, R_d

using ..Vmrs

export AbstractAtmosphericState,
    AtmosphericState,
    ClearAtmosphericState,
    GrayAtmosphericState,
    setup_gray_as_pr_grid,
    setup_gray_as_alt_grid

abstract type AbstractAtmosphericState{FT,I,FTA1D} end

include("GrayAtmosphericStates.jl")

"""
    AtmosphericState{FT,FTA1D,FTA1DN,FTA2D,CLDP,CLDM,VMR,I} <:
        AbstractAtmosphericState{FT,I,FTA1D}

Atmospheric conditions, used to compute optical properties.

# Fields
$(DocStringExtensions.FIELDS)
"""
struct AtmosphericState{
    FT<:AbstractFloat,
    FTA1D<:AbstractArray{FT,1},
    FTA1DN<:Union{AbstractArray{FT,1},Nothing},
    FTA2D<:AbstractArray{FT,2},
    CLDP<:Union{AbstractArray{FT,2},Nothing},
    CLDM<:Union{AbstractArray{Bool,2},Nothing},
    VMR<:AbstractVmr{FT},
    I<:Int,
} <: AbstractAtmosphericState{FT,I,FTA1D}
    "longitude (`ncol`), optional"
    lon::FTA1DN
    "latitude (`ncol`), optional"
    lat::FTA1DN
    "Layer pressures `[Pa, mb]`; `(nlay,ncol)`"
    p_lay::FTA2D
    "Level pressures `[Pa, mb]`; `(nlay+1,ncol)`"
    p_lev::FTA2D
    "Layer temperatures `[K]`; `(nlay,ncol)`"
    t_lay::FTA2D
    "Level temperatures `[K]`; `(nlay+1,ncol)`"
    t_lev::FTA2D
    "Surface temperatures `[K]`; `(ncol)`"
    t_sfc::FTA1D
    "Number of molecules per cm^2 of dry air `(nlay, ncol)`"
    col_dry::FTA2D
    "volume mixing ratio of all relevant gases"
    vmr::VMR
    "effective radius of cloud liquid particles"
    cld_r_eff_liq::CLDP
    "effective radius of cloud ice particles"
    cld_r_eff_ice::CLDP
    "cloud water path"
    cld_path_liq::CLDP
    "cloud ice path"
    cld_path_ice::CLDP
    "cloud mask"
    cld_mask::CLDM
    "ice roughness, 1 = none, 2 = medium, 3 = rough"
    ice_rgh::I
    "Number of layers."
    nlay::I
    "Number of columns."
    ncol::I
    "Number of gases."
    ngas::I
end
Adapt.@adapt_structure AtmosphericState
end


export Add

"""
    Add{D} <: CompositeBlock{D}
    Add(blocks::AbstractBlock...) -> Add
"""
struct Add{D} <: CompositeBlock{D}
    n::Int
    list::Vector{AbstractBlock{D}}
    Add(n::Int, blocks::Vector{AbstractBlock{D}}) where {D} = new{D}(_check_block_sizes(blocks, n), blocks)
    Add(blocks::Vector{AbstractBlock{D}}) where {D} = new{D}(_check_block_sizes(blocks), blocks)
    Add(n::Int; nlevel=2) = new{nlevel}(n, AbstractBlock{nlevel}[])
end

Add(blocks::NTuple{M,AbstractBlock{D}}) where {M,D} = Add(collect(AbstractBlock{D}, blocks))
Add(n::Int, blocks::AbstractVector{<:AbstractBlock{D}}) where {D} = Add(n, collect(AbstractBlock{D}, blocks))
Add(blocks::AbstractVector{<:AbstractBlock{D}}) where {D} = Add(collect(AbstractBlock{D}, blocks))
Add(blocks::AbstractBlock{D}...) where {D} = Add(collect(AbstractBlock{D}, blocks))
nqudits(add::Add) = add.n

function mat(::Type{T}, x::Add{D}) where {D,T}
    length(x.list) == 0 && return Diagonal(zeros(T, D^x.n))
    mapreduce(x -> mat(T, x), +, x.list)
end

chsubblocks(x::Add, it) = Add(it)

function _apply!(r::AbstractRegister, x::Add)
    isempty(x.list) && return r
    length(x.list) == 1 && return _apply!(r, x.list[])

    res = mapreduce(blk -> _apply!(copy(r), blk), regadd!, x.list[1:end-1])
    _apply!(r, x.list[end])
    regadd!(r, res)
    r
end

export Add

subblocks(x::Add) = x.list
cache_key(x::Add) = map(cache_key, x.list)
Base.copy(x::Add) = Add(x.n, copy(x.list))
Base.similar(c::Add) = Add(c.n, empty!(similar(c.list)))

function Base.:(==)(lhs::Add{D}, rhs::Add{D}) where {D}
    nqudits(lhs) == nqudits(rhs) && (length(lhs.list) == length(rhs.list)) && all(lhs.list .== rhs.list)
end

for FUNC in [:length, :iterate, :getindex, :eltype, :eachindex, :popfirst!, :lastindex]
    @eval Base.$FUNC(x::Add, args...) = $FUNC(subblocks(x), args...)
end

Base.getindex(c::Add{D}, index::Union{UnitRange,Vector}) where {D} =
    Add(c.n, getindex(c.list, index))
Base.setindex!(c::Add{D}, val::AbstractBlock{D}, index::Integer) where {D} =
    (setindex!(c.list, val, index); c)
Base.insert!(c::Add{D}, index::Integer, val::AbstractBlock{D}) where {D} =
    (insert!(c.list, index, val); c)
Base.adjoint(blk::Add{D}) where {D} = Add(blk.n, map(adjoint, subblocks(blk)))

## Iterate contained blocks
occupied_locs(c::Add) =
    (unique(Iterators.flatten(occupied_locs(b) for b in subblocks(c)))...,)

# Additional Methods for Add
Base.push!(c::Add{D}, val::AbstractBlock{D}) where {D} = (_check_block_sizes(c, val); push!(c.list, val); c)

function Base.push!(c::Add{D}, val::Function) where {D}
    push!(c, val(c.n))
end

function Base.append!(c::Add, list)
    for blk in list
        push!(c, blk)
    end
    c
end

function Base.prepend!(c::Add, list)
    for blk in list[end:-1:1]
        insert!(c, 1, blk)
    end
    c
end

YaoBase.ishermitian(ad::Add) = all(ishermitian, ad.list) || ishermitian(mat(ad))


#=
Solitair
Bhupendra Aole
10/20/2020
=#
using Random

#= cards:
    ♠ 1:13
    ♡ 14:26
    ♣ 27:39
    ♢ 40:52
=#
CARD = ["A♠", "2♠", "3♠", "4♠", "5♠", "6♠", "7♠", "8♠", "9♠", "T♠", "J♠", "Q♠", "K♠",
        "A♡", "2♡", "3♡", "4♡", "5♡", "6♡", "7♡", "8♡", "9♡", "T♡", "J♡", "Q♡", "K♡",
        "A♣", "2♣", "3♣", "4♣", "5♣", "6♣", "7♣", "8♣", "9♣", "T♣", "J♣", "Q♣", "K♣",
        "A♢", "2♢", "3♢", "4♢", "5♢", "6♢", "7♢", "8♢", "9♢", "T♢", "J♢", "Q♢", "K♢"]
SUIT = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
        1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
        2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,]
BACK = ["▮ ¹", "▮ ²", "▮ ³", "▮ ⁴", "▮ ⁵", "▮ ⁶", "▮ ⁷", "▮ ⁸", "▮ ⁹", "▮ ¹⁰",
        "▮ ¹¹", "▮ ¹²", "▮ ¹³", "▮ ¹⁴", "▮ ¹⁵", "▮ ¹⁶", "▮ ¹⁷", "▮ ¹⁸", "▮ ¹⁹", "▮ ²⁰",
        "▮ ²¹", "▮ ²²", "▮ ²³", "▮ ²⁴"]
# card stacks
closed = []
opened = []
completed = [[], [], [], []] # 4 completed stacks
lane = [[], [], [], [], [], [], []] # 7 stacks for closed in lane
working = [[], [], [], [], [], [], []] # 7 working stacks open over lane

function new_game()
    deck = Array(1:52)
    shuffle!(deck)
    for i in 1:7
        for j in 1:i-1
            push!(lane[i], pop!(deck))
        end
        push!(working[i], pop!(deck))
    end
    global closed = deck
end

function display()
    println()
    println("------------------")
    println("X\tO\t\tA\tB\tC\tD")
    # closed stack
    if length(closed)>0
        print(BACK[length(closed)], "\t")
    else
        print("\t")
    end
    # opened stack
    if length(opened)>0
        print(CARD[opened[length(opened)]], "\t\t")
    else
        print("\t\t")
    end
    for c in completed
        if length(c)>0
            print(CARD[c[length(c)]], "\t")
        else
            print("\t")
        end
    end
    println("\n")
    println("1\t2\t3\t4\t5\t6\t7")
    l = deepcopy(lane)
    w = deepcopy(working)

    # first row
    for i = 1:7
        if length(l[i])>0
            print(BACK[length(l[i])], "\t")
        else
            if length(w[i])>0
                wn = popfirst!(w[i])
                print(CARD[wn], "\t")
            else
                print("\t")
            end
        end
    end
    println()

    # reset of the rows
    conti = true
    while conti
        conti = false
        for i = 1:7
            if length(w[i])>0
                wn = popfirst!(w[i])
                print(CARD[wn], "\t")
                conti = true
            else
                print("\t")
            end
        end
        println()
    end
end

function push_all(s, c)
    if isa(c, Array)
        while length(c)>0
            push!(s, popfirst!(c))
        end
    else
        push!(s, c)
    end
end

function place(cards) # card
    # check if it is a single card or an array with a single card
    if isa(cards, Array) && length(cards)==1
        cards = cards[1]
    end

    if isa(cards, Array)
        c = cards[1]
    else # try completed stack if single card is placed
        c = cards
        if c % 13 == 1 # ACE
            for i = 1:4
                if length(completed[i]) == 0
                    push_all(completed[i], c)
                    return true
                end
            end
        else
            for i = 1:4
                if length(completed[i]) > 0 && completed[i][length(completed[i])]+1 == c
                    push_all(completed[i], c)
                    return true
                end
            end
        end
    end

    # check lanes
    for i = 1:7
        if c%13==0 && length(lane[i])==0 && length(working[i])==0 # place king
            push_all(working[i], cards)
            return true
        elseif length(working[i])>0 && working[i][length(working[i])]%13 == (c+1)%13
            # check suit
            t = working[i][length(working[i])]
            if SUIT[t] != SUIT[c]
                push_all(working[i], cards)
                return true
            end
        end
    end

    return false
end

function is_win()
    for i = 1:7
        if length(lane[i]) != 0
            return false
        end
    end
    return true
end

println("Solitair")
new_game()
while true
    display()
    print(">> ")
    reply = lowercase(strip(readline()))

    if length(reply)==0 || reply == "x"
        if length(closed)>0
            push!(opened, pop!(closed))
        else
            global closed = reverse(opened)
            global opened = []
            push!(opened, pop!(closed))
        end
    elseif reply == "end"
        break
    elseif reply == "o"
        if length(opened)>0
            p = opened[length(opened)]
            if place(p)
                pop!(opened)
            end
        end
    elseif length(reply) == 2
        if occursin(reply[1], "1234567")
            if occursin(reply[2], "123456789")
                l = parse(Int32, reply[1])
                w = working[l]
                if length(w)>0
                    n = parse(Int32, reply[2]) # only take these many cards from the top of the stack
                    if place(w[length(w)-(n-1):length(w)])
                        for i = 1:n
                            pop!(w)
                        end
                        if length(lane[l]) > 0 && length(w) == 0
                            push!(w, pop!(lane[l]))
                        end
                    end
                end
            end
        end
    elseif occursin(reply, "1234567")
        l = parse(Int32, reply)
        w = working[l]
        if length(w)>0
            if place(w)
                empty!(w)
                if length(lane[l])>0
                    push!(w, pop!(lane[l]))
                end
            end
        end
    elseif occursin(reply, "abcd")
        w = completed[Int(reply[1])-96]
        if length(w)>0
            if place(w[length(w)])
                pop!(w)
            end
        end
    end

    if is_win()
        display()
        println("You Win!")
        break
    end
end


@with_kw mutable struct PAPOMCPOWSolver{RNG<:AbstractRNG} <: POMCPOW.AbstractPOMCPSolver
    belief_updater::Updater     = nothing
    eps::Float64                = 0.01
    max_depth::Int              = typemax(Int)
    criterion                   = POMCPOW.MaxUCB(1.0)
    final_criterion             = POMCPOW.MaxQ()
    tree_queries::Int           = 1000
    max_time::Float64           = Inf
    rng::RNG                    = Random.GLOBAL_RNG
    node_sr_belief_updater      = POMCPOW.POWNodeFilter()

    estimate_value::Any         = POMCPOW.RolloutEstimator(POMCPOW.RandomSolver(rng))

    enable_action_pw::Bool      = true
    check_repeat_obs::Bool      = true
    check_repeat_act::Bool      = true
    tree_in_info::Bool          = false

    alpha_observation::Float64  = 0.5
    k_observation::Float64      = 10.0
    alpha_action::Float64       = 0.5
    k_action::Float64           = 10.0
    init_V::Any                 = 0.0
    init_N::Any                 = 0
    next_action::Any            = RandomActionGenerator(rng)
    default_action::Any         = POMCPOW.ExceptionRethrow()
end

mutable struct PAPOMCPOWPlanner{P,NBU,C,NA,SE,IN,IV} <: Policy
    solver::PAPOMCPOWSolver
    problem::P
    node_sr_belief_updater::NBU
    criterion::C
    next_action::NA
    solved_estimate::SE
    init_N::IN
    init_V::IV
    tree::Union{Nothing, POMCPOWTree} # this is just so you can look at the tree later
end

function PAPOMCPOWPlanner(solver, problem::POMDP)
    PAPOMCPOWPlanner(solver,
                  problem,
                  solver.node_sr_belief_updater,
                  solver.criterion,
                  solver.next_action,
                  POMCPOW.convert_estimator(solver.estimate_value, solver, problem),
                  solver.init_N,
                  solver.init_V,
                  nothing)
end

POMDPs.action(pomcp::PAPOMCPOWPlanner, b) = first(action_info(pomcp, b))

function POMCPOW.make_tree(p::PAPOMCPOWPlanner{P, NBU}, b) where {P, NBU}
    S = POMCPOW.statetype(P)
    A = POMCPOW.actiontype(P)
    O = POMCPOW.obstype(P)
    B = POMCPOW.belief_type(NBU,P)
    return POMCPOWTree{B, A, O, typeof(b)}(b, 2*min(100_000, p.solver.tree_queries))
end

function action_info(pomcp::PAPOMCPOWPlanner{P,NBU}, b; tree_in_info=false) where {P,NBU}
    A = actiontype(P)
    info = Dict{Symbol, Any}()
    tree = POMCPOW.make_tree(pomcp, b)
    pomcp.tree = tree
    local a::A
    try
        a = POMCPOW.search(pomcp, tree, b, info)
        if pomcp.solver.tree_in_info || tree_in_info
            info[:tree] = tree
        end
    catch ex
        println("EXCP")
        a = convert(A, default_action(pomcp.solver.default_action, pomcp.problem, b, ex))
    end
    return a, info
end

function POMCPOW.search(pomcp::PAPOMCPOWPlanner, tree::POMCPOWTree, b, info::Dict{Symbol,Any}=Dict{Symbol,Any}())
    all_terminal = true
    i = 0
    start_us = CPUtime_us()
    for i in 1:pomcp.solver.tree_queries
        s = rand(pomcp.solver.rng, tree.root_belief)
        if !POMDPs.isterminal(pomcp.problem, s)
            max_depth = min(pomcp.solver.max_depth, ceil(Int, log(pomcp.solver.eps)/log(discount(pomcp.problem))))
            simulate(pomcp, POWTreeObsNode(tree, 1), s, b, max_depth)
            all_terminal = false
        end
        if CPUtime_us() - start_us >= pomcp.solver.max_time*1e6
            break
        end
    end
    info[:search_time_us] = CPUtime_us() - start_us
    info[:tree_queries] = i

    if all_terminal
        throw(AllSamplesTerminal(tree.root_belief))
    end

    best_node = POMCPOW.select_best(pomcp.solver.final_criterion, POWTreeObsNode(tree,1), pomcp.solver.rng)

    return tree.a_labels[best_node]
end

function simulate(pomcp::PAPOMCPOWPlanner, h_node::POWTreeObsNode{B,A,O}, s::S, b, d) where {B,S,A,O}
    tree = h_node.tree
    h = h_node.node

    sol = pomcp.solver

    if POMDPs.isterminal(pomcp.problem, s) || d <= 0
        return 0.0
    end

    if sol.enable_action_pw
        total_n = tree.total_n[h]
        if length(tree.tried[h]) <= sol.k_action*total_n^sol.alpha_action
            a = POMCPOW.next_action(pomcp.next_action, pomcp.problem, b, POWTreeObsNode(tree, h))
            if !sol.check_repeat_act || !haskey(tree.o_child_lookup, (h,a))
                POMCPOW.push_anode!(tree, h, a,
                            init_N(pomcp.init_N, pomcp.problem, POWTreeObsNode(tree, h), a),
                            init_V(pomcp.init_V, pomcp.problem, POWTreeObsNode(tree, h), a),
                            sol.check_repeat_act)
            end
        end
    else # run through all the actions
        if isempty(tree.tried[h])
            action_space_iter = POMDPs.actions(pomcp.problem, s)
            anode = length(tree.n)
            for a in action_space_iter
                push_anode!(tree, h, a,
                            init_N(pomcp.init_N, pomcp.problem, POWTreeObsNode(tree, h), a),
                            init_V(pomcp.init_V, pomcp.problem, POWTreeObsNode(tree, h), a),
                            false)
            end
        end
    end
    total_n = tree.total_n[h]

    best_node = POMCPOW.select_best(pomcp.criterion, h_node, pomcp.solver.rng)
    a = tree.a_labels[best_node]

    new_node = false
    if tree.n_a_children[best_node] <= sol.k_observation*(tree.n[best_node]^sol.alpha_observation)

        sp, o, r = gen(DDNOut(:sp, :o, :r), pomcp.problem, s, a, sol.rng)

        if sol.check_repeat_obs && haskey(tree.a_child_lookup, (best_node,o))
            hao = tree.a_child_lookup[(best_node, o)]
        else
            new_node = true
            hao = length(tree.sr_beliefs) + 1
            push!(tree.sr_beliefs,
                  POMCPOW.init_node_sr_belief(pomcp.node_sr_belief_updater,
                                      pomcp.problem, s, a, sp, o, r))
            push!(tree.total_n, 0)
            push!(tree.tried, Int[])
            push!(tree.o_labels, o)

            if sol.check_repeat_obs
                tree.a_child_lookup[(best_node, o)] = hao
            end
            tree.n_a_children[best_node] += 1
        end
        push!(tree.generated[best_node], o=>hao)
    else

        sp, r = gen(DDNOut(:sp, :r), pomcp.problem, s, a, sol.rng)

    end

    if r == Inf
        @warn("POMCPOW: +Inf reward. This is not recommended and may cause future errors.")
    end

    if new_node
        R = r + POMDPs.discount(pomcp.problem)*POMCPOW.estimate_value(pomcp.solved_estimate, pomcp.problem, sp, POWTreeObsNode(tree, hao), d-1)
    else
        pair = rand(sol.rng, tree.generated[best_node])
        o = pair.first
        hao = pair.second
        POMCPOW.push_weighted!(tree.sr_beliefs[hao], pomcp.node_sr_belief_updater, s, sp, r)
        sp, r = rand(sol.rng, tree.sr_beliefs[hao])

        bp = POMDPs.update(pomcp.solver.belief_updater, b, a, o)
        R = r + POMDPs.discount(pomcp.problem)*simulate(pomcp, POWTreeObsNode(tree, hao), sp, bp, d-1)
    end

    tree.n[best_node] += 1
    tree.total_n[h] += 1
    if tree.v[best_node] != -Inf
        tree.v[best_node] += (R-tree.v[best_node])/tree.n[best_node]
    end

    return R
end

function POMDPs.solve(solver::PAPOMCPOWSolver, problem::POMDP)
    return PAPOMCPOWPlanner(solver, problem)
end