module CareData
    using StatsBase
    using Statistics

    using FileIO: load
    using DataFrames
    using Query
    using IterableTables
    using CSVFiles
    using CSV
    using Logging

    using IteratorInterfaceExtensions
    import TableTraits: isiterabletable

    using GlobalDecls
    using ONSCodes
    using Utils
    import LAModelData: get_staying_put_rates, get_18s_level_from_doe

    export load_all
    export get_yp, make_yp_frame, make_carer_frame
    export Carer, YP
    export create_base_datasets, add_carer_to_frame!
    export DataSettings, default_data_settings
    export CPIINDEX, AFC_SURVEY_YEAR, THIS_YEAR, uprate
    export CarerOutcomes, addcareroutcomestoframe!, makecareroutcomesframe
    export fillcomponents!

    THIS_YEAR=2020
    AFC_SURVEY_YEAR=2018

    MINIMUM_WAGE_2019 = 6.15*40 # 18-20
 	JSA_IS_2019 = 57.90

    MINIMUM_WAGE_2020 = 6.45*40 # 18-20
 	JSA_IS_2020 = 57.90 # FIXME unchanged 2019/20 rate

    MINIMUM_WAGE=MINIMUM_WAGE_2020
 	JSA_IS=JSA_IS_2020

    @enum EmploymentStatus TrainingOrEmployment HigherEd OtherEd NEET Unknown
    @enum DataPublisher OFSTED DFE AFC

    export EmploymentStatus, TrainingOrEmployment, HigherEd, OtherEd, NEET, Unknown
    export DataPublisher, OFSTED, DFE, AFC

    function efroms( s :: AbstractString ) :: EmploymentStatus
        t :: EmploymentStatus = Unknown
        if s == "TrainingOrEmployment"
            t = TrainingOrEmployment
        elseif s == "HigherEd"
            t = HigherEd
        elseif s == "OtherEd"
            t = OtherEd
        elseif s == "NEET"
            t = NEET
        else
            t = Unknown
        end
        t
    end

    function efroms( s :: EmploymentStatus)::EmploymentStatus
        s
    end

    #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 whose activity is training or employment
    # from DFe CareLeavers_Act_InTouch19to212018_amended.csv
    CL_All_19to21_2018  = 28_510 # all
    ED_PCS_2018 = (
        #  Care leavers in the year ending 31 March 2018 aged 19 to 21 who were looked after for a total of at least 13 weeks after their 14th birthday including some time after their 16th birthday [1]
        :CL_Act_TE19to21_pc => 25, # 25

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 whose activity is higher education i.e. studies beyond A level
        :CL_Act_HE19to21_pc => 6, # HE 6

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 whose activity is education other than higher education
        :CL_Act_OE19to21_pc => 20, # FE 20

        # Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 who are not in education, employment or training
        :CL_Act_NEET19to21_pc => 39, # NEET 39

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 for whom local authority does not have activity information
        :CL_Act_NoInf19to21_pc => 10 )
         #No inf 10
    # 2019 version
    CL_All_19to21_2019  = 29_930 # all
    ED_PCS_2019 = (
        #  Care leavers in the year ending 31 March 2018 aged 19 to 21 who were looked after for a total of at least 13 weeks after their 14th birthday including some time after their 16th birthday [1]
        :CL_Act_TE19to21_pc => 25.0, # 25

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 whose activity is higher education i.e. studies beyond A level
        :CL_Act_HE19to21_pc => 6.0, # HE 6

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 whose activity is education other than higher education
        :CL_Act_OE19to21_pc => 21.0, # FE 20

        # Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 who are not in education, employment or training
        :CL_Act_NEET19to21_pc => 39.0, # NEET 39

        #  Percentage of care leavers in the year ending 31 March 2018 aged 19 to 21 for whom local authority does not have activity information
        :CL_Act_NoInf19to21_pc => 9.0 ) #No inf 10

    ED_PCS = ED_PCS_2019 # FIXME move this to a function
    CL_All_19to21 = CL_All_19to21_2019

    function educstatus( ccode :: AbstractString ) :: EmploymentStatus
        # FIXME should use LA level stuff and a search
        # FIXME 2020 needed
        e ::EmploymentStatus = NEET
        r = rand(1:90)
        if( r <= 25)
            e = TrainingOrEmployment
        elseif( r <= 31 )
            e = HigherEd
        elseif( r <= 51 )
            e = OtherEd
        end
        e
    end

    # from ofsted ss 2019, addn table 1
    # we'll use this is proportions in each skill class
    # Approved for one type of care	20,260
    # Approved for two types of care	12,735
    # Approved for three types of care	4,905
    # Approved for four or more types of care	5,575
    # then (Main Table, col1 4920 new approved - assume level 0 )
    SKILLS_LEVELS_2018 = [5000, 20060.0-5000,12735.0,4905.0,5575.0]
    SKILLS_PROPS_2018 = cumsum(SKILLS_LEVELS_2018)/sum(SKILLS_LEVELS_2018)

    # 5_110 is 8400 approved - 3290 friends and family
    SKILLS_LEVELS_2019 = [5_110, 20_755.0-5000,13_510.0,5_200.0,4_985.0]
    SKILLS_PROPS_2019 = cumsum(SKILLS_LEVELS_2019)/sum(SKILLS_LEVELS_2019)

    SKILLS_LEVELS=SKILLS_LEVELS_2019
    SKILLS_PROPS=SKILLS_PROPS_2019

    function skilllevel()
        r = rand()
        nsk = size( SKILLS_PROPS )[1]
        for i in 1:nsk
            if r <= SKILLS_PROPS[i]
                return i
            end
        end
    end

    # actual and forecast CPI index rebased to 2019=100; from ONS chart 3.18
    # FIXME unchanged for update
    CPIINDEX = Dict(
        2010 => 0.816463685631,
        2011 => 0.843392062033,
        2012 => 0.881038496092,
        2013 => 0.905956397748,
        2014 => 0.929192356761,
        2015 => 0.942768148942,
        2016 => 0.943146193059,
        2017 => 0.949367808928,
        2018 => 0.974840594465,
        2019 => 1.00,
        2020 => 1.020495466904,
        2021 => 1.040612242049,
        2022 => 1.062698654012,
        2023 => 1.084820802953,
        2024 => 1.106897548551,
        2025 => 1.129423568993,
        2026 => 1.152408007287,
        2027 => 1.175860192508,
        2028 => 1.199789643583,
        2029 => 1.224206073155,
        2030 => 1.249119391524,
        2031 => 1.274539710671,
        2032 => 1.300030504884,
        2033 => 1.326031114982

    )

    function uprate( x :: Real, newyear :: Integer, oldyear :: Integer = -1 ) :: Real
        if oldyear == -1
            oldyear = newyear-1
        end
        x * (CPIINDEX[newyear]/CPIINDEX[oldyear])
    end

    # from OFSTED
    total_in_placements = [50600,51315,51805,51805,52005,53040]
    total_reaching_18 = [3160,3335,3435,4025,3430,3435]
    age18_and_remain = [1685, 1750, 1790, 2190, 1570, 1695]

    ny = size( total_in_placements )[1]
    # @info  "size $ny"
    growth = zeros( ny-1 )

    for i in 1:ny-1
        growth[i]=1+(total_in_placements[i+1]-total_in_placements[i])/total_in_placements[i]
    end

    function make_yp_frame( n::Integer )
        DataFrame(
            year          = zeros( Integer, n ),
            ccode         = Vector{AbstractString}(undef,n),
            carer         = zeros( Integer, n ),
            age           = zeros( Integer, n ),
            housing_costs = zeros( n ),
            earnings      = zeros( n ),
            benefits      = zeros( n ),
            employment_status = Vector{EmploymentStatus}(undef, n ))
    end

    function make_carer_frame( n::Integer )
        DataFrame(
            year              = zeros( Integer, n ),
            ccode             = Vector{AbstractString}(undef,n),
            id                = zeros( Integer, n ),
            age               = zeros( Integer, n ),
            housing_costs     = zeros( n ),
            earnings          = zeros( n ),
            benefits          = zeros( n ),
            employment_status = zeros( Integer, n ),
            skill_level       = zeros( Integer, n ))
     end

     mutable struct Carer
        id                :: Integer
        age               :: Integer
        housing_costs     :: Real
        earnings          :: Real
        benefits          :: Real
        employment_status :: Integer
        skill_level       :: Integer
     end

     function carerfromrow( i ) :: Carer
         Carer( i.id, i.age, i.housing_costs, i.earnings, i.benefits, i.employment_status, i.skill_level )
     end

     function ypfromrow( q ) :: YP
        YP( q.age, q.housing_costs, q.earnings, q.benefits, efroms( q.employment_status ))
     end

     function get_carers(
        carer_dataset,
        ccode :: AbstractString,
        year  :: Integer ) :: Array{Carer}
        @assert isiterabletable( carer_dataset ) "data needs to implement IterableTables"
        q = @from i in carer_dataset begin
            @where (i.ccode == ccode) && (i.year == year)
            @select i
            @collect
        end
        lq = length( q )[1]
        # @info  "lq = $lq"
        carers = Array{Carer}( undef, lq )
        j = 0
        for i in q
            j += 1
            carers[j] = Carer( i.id, i.age, i.housing_costs, i.earnings, i.benefits, i.employment_status, i.skill_level )
        end
        carers
     end # get_carers

    function add_carer_to_frame!( yp_dataset, year ::Integer, ccode :: AbstractString, carer :: Carer )
        @assert isiterabletable( yp_dataset ) "data needs to implement IterableTables; is "*typeof(yp_dataset)
        d = [ year, ccode, carer.id, carer.age, carer.housing_costs,
             carer.earnings, carer.benefits, carer.employment_status, carer.skill_level]
        push!( yp_dataset, d )
    end

    mutable struct CarerOutcomes
        payments_from_la      :: Real
        income_recieved       :: Real
        contributions_from_yp :: Real
        housing_cont          :: Real
        other_cont            :: Real
        fee                   :: Real
        allowance             :: Real
    end

    function CarerOutcomes(; fee :: Real, allowance::Real, housing_cont :: Real, other_cont :: Real ) :: CarerOutcomes
        c = CarerOutcomes( 0.0, 0.0, 0.0, housing_cont, other_cont, fee, allowance )
        fillcomponents!( c )
        c
    end

    function fillcomponents!( c :: CarerOutcomes )
        c.contributions_from_yp =
            c.housing_cont + c.other_cont
        c.income_recieved = c.fee + c.allowance
        c.payments_from_la = max( 0.0, c.income_recieved - c.contributions_from_yp)
    end

    import Base.≈

    function ≈(left :: CarerOutcomes, right::CarerOutcomes )::Bool
        ( left.payments_from_la ≈ right.payments_from_la ) &&
        ( left.contributions_from_yp ≈ right.contributions_from_yp ) &&
        ( left.housing_cont ≈ right.housing_cont ) &&
        ( left.income_recieved ≈ right.income_recieved )
    end

    function makecareroutcomesframe( n :: Integer )
        DataFrame(
            sysno         = zeros( Integer, n ),
            iteration     = zeros( Integer, n ),
            year          = zeros( Integer, n ),
            ccode         = Vector{AbstractString}(undef,n),
            rcode         = Vector{AbstractString}(undef,n),
            carer         = zeros( Integer, n ),
            yp_age        = zeros( Integer, n ),
            payments_from_la      = zeros( n ),
            contributions_from_yp = zeros( n ),
            income_recieved       = zeros( n )
            )
    end

    function addcareroutcomestoframe!(
        out_dataset,
        sysno :: Integer,
        iteration :: Integer,
        year  :: Integer,
        ccode :: AbstractString,
        rcode :: AbstractString,
        carerid::Integer,
        yp_age :: Integer,
        outcomes :: CarerOutcomes )
        @assert isiterabletable( out_dataset ) "data needs to implement IterableTables; is "*typeof(out_dataset)
        d = [ sysno, iteration, year, ccode, rcode, carerid, yp_age, outcomes.payments_from_la, outcomes.contributions_from_yp, outcomes.income_recieved]
        push!( out_dataset, d )
    end

    function make_council_frame( n::Integer )
        DataFrame(
            name  = Vector{AbstractString}(undef,n),
            ccode = Vector{AbstractString}(undef,n),
            rcode = Vector{AbstractString}(undef,n))
    end

    mutable struct YP
        age :: Integer
        housing_costs :: Real
        earnings      :: Real
        benefits      :: Real
        employment_status :: EmploymentStatus
    end

    function makelaoutcomesframe( n::Integer )
        DataFrame(
            year      = zeros( Integer, n ),
            iteration = zeros( Integer, n ),
            council   = Vector{AbstractString}(undef,n),
            ccode     = Vector{AbstractString}(undef,n),
            rcode     = Vector{AbstractString}(undef,n),
            incomes   = zeros(n),
            payments  = zeros(n),
            grant_allocations = zeros(n))
    end

    function ageyp!( yp :: YP, year :: Integer )
        yp.age += 1
        yp.housing_costs = uprate(yp.housing_costs, year)
        yp.earnings = uprate(yp.earnings, year)
        yp.benefits = uprate(yp.benefits, year)
    end

    function agecarer!( carer :: Carer, year :: Integer )
        carer.age += 1
        carer.housing_costs = uprate(carer.housing_costs, year)
        carer.earnings = uprate(carer.earnings, year)
        carer.benefits = uprate(carer.benefits, year)
    end

    function get_yp(
        yp_dataset,
        year       :: Integer,
        carer      :: Carer,
        yearmatch  = nothing ) :: Union{YP,Nothing}
        @assert isiterabletable( yp_dataset ) "data needs to implement IterableTables"
        hits=[]
        if yearmatch !== nothing
            hits = (yp_dataset.carer .== carer.id) .& yearmatch
        else
            hits = (yp_dataset.carer .== carer.id) .& (yp_dataset.year .== year)
        end
        n = 0
        yp = nothing
        for q in eachrow(yp_dataset[hits,:])
            yp = YP( q.age, q.housing_costs, q.earnings, q.benefits, q.employment_status )
            n += 1
        end
        @assert n <= 1
        yp
    end

    """
    looks up a random cat A LHA allowance for this LA. (Random since you can have > 1 BHA for each LA)
    if missing returns the mean of all values in CAT A for 2019  LHA Tables 2019/20
    see: https://www.gov.uk/government/publications/local-housing-allowance-lha-rates-applicable-from-april-2019-to-march-2020
    see: http://sticerd.lse.ac.uk/case/ for what we really want to do here
    """
    function get_shared_accommodation_rate( ccode :: AbstractString )::Real
        v = ONSCodes.get_brma_value( ccode )
        if v < 0.0
            v = 69.16
        end
        v
    end

    function get_yp2( yp_dataset, year :: Integer, carer :: Carer ) :: Union{YP,Nothing}
        @assert isiterabletable( yp_dataset ) "data needs to implement IterableTables"
        q = @from i in yp_dataset begin
            @where i.carer == carer.id && i.year == year
            @select i
            @collect
        end
        n = size( q )[1]
        @assert n <= 1
        if n == 1
            q = q[1]  # handle as a dataframe row
            YP( q.age, q.housing_costs, q.earnings, q.benefits, q.employment_status )
        end
    end

    function addyptoframe!( yp_dataset, year ::Integer, ccode :: AbstractString, carerid::Integer, q :: YP )
        @assert isiterabletable( yp_dataset ) "data needs to implement IterableTables"
        d = [ year, ccode, carerid, q.age, q.housing_costs, q.earnings, q.benefits, q.employment_status]
        push!( yp_dataset, d )
    end

    function newperson()::YP
        YP( 18,0.0,0.0, Unknown )
    end

    mutable struct DataSettings
        name      :: AbstractString
        dataset   :: AbstractString
        description :: AbstractString
        agglevel  :: AggLevel
        poolyears :: Bool
        avggrowth :: Real
        prp_reach_18 :: Real
        startyear :: Integer
        endyear   :: Integer
        annual_sp_increment :: Real
        num_iterations :: Integer
        targets   :: Array{AbstractString}
        reaching_18s_source :: DataPublisher
        datayear  :: Integer
    end

    function default_data_settings()::DataSettings
        DataSettings(
            "",
            "",
            "Description of the run",
            local_authority,
            false,
            mean(growth),
            (total_reaching_18./total_in_placements)[ny],
            2017,
            2026,
            0.0,
            1,
            [],
            OFSTED,
            THIS_YEAR )
    end

    function add_aged_data!(
        ;
        yp_dataset,
        carer_dataset,
        pid       :: Integer,
        ccode     :: AbstractString,
        ladata    :: Dict,
        stayingrates :: Vector{<:Real},
        startyear :: Integer
        )
        @assert isiterabletable( yp_dataset ) "yp_dataset needs to implement IterableTables"
        @assert isiterabletable( carer_dataset ) "carer_dataset needs to implement IterableTables"

        employment_status = educstatus( ccode )
        housing = get_shared_accommodation_rate( ccode )*CPIINDEX[startyear]

        benefits = 0.0
        earnings = 0.0
        if employment_status == TrainingOrEmployment
            earnings = MINIMUM_WAGE*CPIINDEX[startyear]
        end
        benefits = 0.0
        if employment_status == NEET
            benefits = JSA_IS*CPIINDEX[startyear]
        end
        yp = YP( 18, housing, earnings, benefits, employment_status )
        earnings = 0.0
        employment_status = 0
        skill_level = skilllevel()
        housing = 0.0
        benefits = 0.0
        carer = Carer( pid, 40, housing, earnings, benefits, employment_status, skill_level )
        thisyear = startyear
        @info  "stayingrates=$stayingrates"
        for p in 1:3 ### FIXME parameterise 3
            r = rand()
            stayr = stayingrates[p]
            if r > stayr
                @info  "exiting; r=$r stayrate $stayr "
                break
            end
            addyptoframe!( yp_dataset, thisyear, ccode, carer.id, yp )
            add_carer_to_frame!( carer_dataset, thisyear, ccode, carer )
            ageyp!( yp, thisyear )
            agecarer!( carer, thisyear )
            thisyear += 1
        end
    end # add_aged_data!

    function include_this( ccode :: AbstractString; settings :: DataSettings ) :: Boolean
        if ONSCodes.is_aggregate( ccode )
            return false;
        end
        if ccode in SKIPLIST
            return false;
        end
        if size(settings.targets)[1] == 0
            return true
        end
        return ccode in settings.targets
    end

    function create_base_datasets(
        ofdata   :: DataFrame,
        settings :: DataSettings )
        if size(settings.targets)[1] > 0
            targets  = ofdata.ccode .|> [x->x in settings.targets]
            t = settings.targets
            @info  "settings.targets = $t"
            @info  "targets = $targets"
            ofdata = ofdata[targets,:]
        end
        @info  "settings $settings"
        ofdata.number_of_children_or_young_people_in_placements_at_31_march =
            Missings.replace( ofdata[!,:number_of_children_or_young_people_in_placements_at_31_march],0)
        ncouncils = size( ofdata )[1]
        councils = make_council_frame( ncouncils )
        carer_data = make_carer_frame(0)
        yp_data = make_yp_frame(0)
        nc = 0
        pid = 0
        r = 0
        for ofdat in eachrow(ofdata)
            r += 1
            ccode = ofdat.ccode
            if include_this( ccode, settings )
                nc += 1
                if nc > 2000 # test break if needed
                    break
                end
                councils[:name][nc] = ofdat[:council]
                councils[:ccode][nc] = ccode
                councils[:rcode][nc] = ofdat[:rcode]
                base_staying_rates = get_staying_put_rates(
                    ccode,
                    settings.agglevel,
                    settings.poolyears )
                total_fostered = ofdat[:number_of_children_or_young_people_in_placements_at_31_march]
                if settings.reaching_18s_source == OFSTED
                    new_reached_18_base = settings.prp_reach_18 * total_fostered
                else
                    new_reached_18_base = get_18s_level_from_doe( ccode )
                    if ismissing( new_reached_18_base )
                        # tf = typeof(total_fostered )
                        @warn  "new_reached_18 missing for ccode $ccode"
                        new_reached_18_base = settings.prp_reach_18 * total_fostered # fallback
                    end
                end
                g = 1.0
                for year in (settings.startyear-2):settings.endyear # FIXME hack
                    y = year-2019 # fixme make 2019 some sort of parameter
                    stayingrates = base_staying_rates .+ ( settings.annual_sp_increment*y )
                    @info  "at year $year stayingrates = $stayingrates base_staying_rates $base_staying_rates"
                    g = settings.avggrowth^y
                    new_reached_18 = round(Integer,new_reached_18_base*g)
                    @info  "adding $new_reached_18 people for council $ccode total fostered $total_fostered y=$y g=$g new_reached_18_base=$new_reached_18_base"
                    for i in 1:new_reached_18
                        pid += 1
                        add_aged_data!(
                            yp_dataset = yp_data,
                            carer_dataset = carer_data,
                            pid=pid,
                            ccode=ccode,
                            ladata = Dict(),
                            stayingrates=stayingrates,
                            startyear=year
                        )
                    end # people
                end # year
            end # a valid council
        end # councils
        year_matches = Dict()
        for year in (settings.startyear-2):settings.endyear+2
            year_matches[year] = yp_data.year .== year
        end
        (
            councils     = councils,
            carer_data   = carer_data,
            yp_data      = yp_data,
            year_matches = year_matches )
    end

    function load_all( year :: Integer )::NamedTuple
        ofdata = CSV.File( DATADIR*"edited/$(year)/OFDATA.csv", missingstrings=["x","","-", "c",".", "", "..", "*"],
        types=make_type_block(14:1000)) |> DataFrame
        lcnames = Symbol.(Utils.basiccensor.(string.(names(ofdata))))
        rename!( ofdata, lcnames )
        col = 0
        ofs = size( ofdata )[1]
        for name in lcnames
            col+=1
            if( col >13 )
                for row in 1:ofs
                    if ofdata[row,name] === missing
                        ofdata[row,name] = 0
                    end
                end
            end
        end
        # loadtable( DATADIR*"edited/OFDATA.csv", indexcols=[:ccode] )
        grantdata = load( DATADIR*"edited/$(year)/GRANTS_$(year).csv" ) |> DataFrame
        # loadtable(  DATADIR*"edited/GRANTS_2019.csv", indexcols=[:ccode] )
        lcnames = Symbol.(Utils.basiccensor.(string.(names(grantdata))))
        rename!(grantdata, lcnames )
        (ofdata=ofdata, grantdata=grantdata )
    end # load_all

end


"""
    loaddata(data_type, file_name, f_s, f_center, f_gnss, t_length;
             start_data_idx=1, site_lla=missing, data_start_time=missing)

Loads data from `scN` file and loads into `GNSSData` type struct.

`site_lla` is `[latitude, longitude, height]` in (degrees, degrees, meters).

`data_start_time` is `[year, month, day, hour, minute, seconds]`, where "ss" is
decimal seconds.

Required Arguments:

- `data_type`: one of the following for `Complex{IntN}` datasets
 	* `:sc4`
	* `:sc8`
	* `:sc16`
	* `:sc32`
	* `:sc64`
- `file_name::String`: data file name
- `f_s::Float64`: sampling rate of data in Hz
- `f_center::Float64`: center frequency of the receiver in Hz
- `f_gnss::Float64`: center frequency of the GNSS signal in Hz
- `t_length::Float64`: the length of data in seconds to be loaded

Optional Arguments:

- `skip_to::Float64`: where the first data sample will be loaded in the data
                      `(default = 0. sec)`
	* units are in seconds
- `site_lla`: receiver LLA location in the format `(lat, lon, height)`
	* `lat`, latitude, is in degrees
	* `lon`, longitude, is in degrees
	* `height` is in meters
	* `(default = missing)`
- `data_start_time`: data UTC start time `(year, month, day, hour, minute, seconds)`
	* `seconds` can be `Int` or `Float64`, all others are `Int`
	* `(default = missing)`
- `start_t`: the start time in seconds `(default = 0 seconds)`

Returns:

- `GNSSData` struct
"""
function loaddata(data_type, file_name, f_s, f_center, f_gnss, t_length;
                  skip_to=0., site_loc_lla=missing,
                  data_start_time=missing, start_t=0)
	# Compute number of samples to extract
	sample_num = Int(f_s * t_length)
	# Calculate f_if
	f_if = f_gnss - f_center
	# Determine nADC
	if data_type == :sc4
		data_type = Val(:sc4)
		nADC = 4
		data = Array{Complex{Int8}}(undef, sample_num)
	elseif data_type == :sc8
		data_type = Val(:sc8)
		nADC = 8
		data = Array{Complex{Int8}}(undef, sample_num)
	elseif data_type == :sc16
		data_type = Val(:sc16)
		nADC = 16
		data = Array{Complex{Int16}}(undef, sample_num)
	elseif data_type == :sc32
		data_type = Val(:sc32)
		nADC = 32
		data = Array{Complex{Int32}}(undef, sample_num)
	elseif data_type == :sc64
		data_type = Val(:sc64)
		nADC = 64
		data = Array{Complex{Int64}}(undef, sample_num)
	else
		error("Invalid data type: $(typeof(data_type))")
	end
	# Read data
	start_data_idx = floor(Int, f_s*skip_to)
	data, end_idx, dtype = readdatafile!(data, data_type, file_name,
	                                     sample_num, nADC, start_data_idx)
	# Calculate total data length in seconds
	total_data_length = (end_idx)/f_s
	# Generate time vector
	# t = calctvector(sample_num, f_s)
	return GNSSData(file_name, f_s, f_if, t_length, start_data_idx,
	                data, String(dtype), data_start_time,
	                site_loc_lla, sample_num, total_data_length, nADC, start_t)
end

"""
	reloaddata!(gnss_data::GNSSData, start_data_idx, sample_num)

Reloads portion of data up to the length of the data array inside `gnss_data`.

Required Arguments:

- `gnss_data::GNSSData`: structure that holds loaded data from data files
- `start_data_idx::Int`: where the first data sample will be loaded in the data
- `sample_num::Int`: number of samples after `start_data_idx` to load into
                     `gnss_data`
    * must not be greater than `gnss_data.sample_num`

Modifies in Place and Returns:

- `gnss_data::GNSSData`
"""
function reloaddata!(gnss_data::GNSSData, skip_to)
	f_s = gnss_data.f_s
	nADC = gnss_data.nADC
	start_data_idx = floor(Int, f_s*skip_to)
	sample_num=gnss_data.sample_num
	file_name = gnss_data.file_name
	data_type = gnss_data.data_type
	data = gnss_data.data
	if sample_num <= gnss_data.sample_num
        f = open(file_name, "r")
		byte_num = Int(2*nADC/8)
		seek(f, byte_num*start_data_idx)
		read!(f, data)
		close(f)
	else
		error("`sample_num` is greater than data sample size!")
	end
	return gnss_data
end

"""
	readdatafile!(data, data_type, file_name, sample_num, start_idx=1)

Loads `scN` data files. First N-bit number is real, second is imaginary.

Required Arguments:

- `data::Vector{Complex{IntN}}`: the complex data vector to store the new data
- `data_type::Val{:scN}`: only accepts `Val(:scN)` to indicate data is N-bit
                          complex
- `file_name::String`: data file name
- `sample_num::Int`: `[NOT USED]` number of samples after `start_data_idx` to
                     load

Optional Arguments:

- `start_idx::Int`: where the first data sample will be loaded in the data
                    `(default = 0)`

Returns:

- `data::Vector{Complex{IntN}}`: elements are replaced with new data
- `end_idx::Int`: the last sample index loaded plus one
- `:scN::Symbol`: symbol which describes the type of data
"""
function readdatafile!(data, data_type, file_name, sample_num,
                       nADC, start_idx=0)
	# Open file
	f = open(file_name, "r")
	# Go to start location
	byte_num = 2*Int(nADC/8)
	seek(f, byte_num*start_idx)
	# Load Complex{IntN} values directly from file
	read!(f, data)
	# Get the index value for the end of the file
	end_idx = Int(position(seekend(f))/byte_num)
    close(f)
    return (data, end_idx, gnsstypes[data_type])
end

"""
	readdatafile!(data, data_type::Val{:sc4}, file_name, sample_num,
				  start_idx=1)

Loads `sc4` data files. For each UInt8 number, the LSB is real and MSB is
imaginary.

**NOTE:** this method is slower than the version used for 8-bit complex data.

Required Arguments:

- `data::Vector{Complex{Int}}`: the complex data vector to store the new data
- `data_type::Val{:sc4}`: only accepts `Val(:sc4)` to indicate data is 4-bit
                          complex
- `file_name::String`: data file name
- `sample_num::Int`: `[NOT USED]` number of samples after `start_data_idx` to
                     load

Optional Arguments:

- `start_idx::Int`: where the first data sample will be loaded in the data
	                `(default = 0)`

Returns:

- `data::Vector{Complex{Int}}`: elements are replaced with new data
- `end_idx::Int`: the last sample index loaded plus one
- `:sc4::Symbol`: symbol which describes the type of data
"""
function readdatafile!(data, data_type::Val{:sc4}, file_name, sample_num,
                       nADC, start_idx=0)
	# Open file
	f = open(file_name, "r")
	# Go to start location
	seek(f, start_idx)
	# Load UInt8 values and convert to Complex{Int8}
	data[:] = bytetocomplex.(read(f, sample_num))
	# Get the index value for the end of the file
	end_idx = position(seekend(f))
    close(f)
    return (data, end_idx, :sc4)
end

"""
    bytetocomplex(byte::UInt8)

Converts a `UInt8` value to `Complex{Int8}`. Assumes that the LSB and MSB are the
real and imaginary parts, respectively.

Required Arguments:

- `byte::UInt8`: a single byte to converted to `Complex{Int8}`

Returns:

- `Complex{Int8}`: complex number converted from `byte`
"""
function bytetocomplex(byte::UInt8)
	# Get unsigned I and Q values
	Iuns = byte & 0x0f
	Quns = byte >> 0x04
	# Get I and Q signs
	Isign = (Iuns&0x08) >> 0x03
	Qsign = (Quns&0x08) >> 0x03
	# Get I and Q magnitudes
	Imag = xor(Isign*0x07, Iuns&0x07)
	Qmag = xor(Qsign*0x07, Quns&0x07)
	# Compute the complex number
	return (-1)^(Isign)*Imag + (-1)^(Qsign)*Qmag*1im
end

"""
	FileInfo

`Struct` that holds data file information based off its file name.

Fields:

- `f_s::Float64`: sampling rate of signal in Hz
- `f_if::Float64`: signal IF frequency in Hz
- `f_center::Float64`: receiver center frequency in Hz
- `sig_freq::Float64`: navigation band frequency in Hz
                       (e.g. for L1, `sig_freq = L1_freq`)
- `sigtype::T1`: `[DEPRICATED]`
- `data_type::T2`: the data type of the data file which is either set to
                   `Val(:sc4)` or `Val(:sc8)`
- `timestamp::T3`: six element `Tuple` with format
                   `(year, month, day, hour, minute, second)` where everything
				   other than `second`, which can be either `Int` or `Float64`,
				   is an Int
- `timestamp_JD::Float64`: the Julia date of the timestamp
- `file_name::String`: file name of the data file
"""
struct FileInfo{T1,T2,T3}
	f_s::Float64
	f_if::Float64
	f_center::Float64
	sig_freq::Float64
	sigtype::T1
	data_type::T2
	timestamp::T3
	timestamp_JD::Float64
	file_name::String
end

"""
	data_info_from_name(file_name)

Determine the IF and sampling frequency of the data from the file name. Looks
for the following strings inside the file name:

- `g1b1`: 25 Msps, centered at 1.57 GHz which may contain the followin signals
	* GPS L1
	* Galileo E1bc
	* Beidou B1i
- `g2r2`: `[NOTE SUPPORTED]` 25 Msps, centered at 1.2375 GHz which may contain
          the followin signals
	* GPS L2
	* GLONASS L2
- `g5`: 25 Msps, centered at 1.17645 GHz which may contain the followin signals
	* GPS L5
	* Galileo E5a

Also looks at the file name extension to determine the type of data stored in
the file where

- `.sc4`: 4-bit complex data where for each UInt8 number in the data file, the
  LSB is real and MSB is imaginary
- `.sc8`: 8-bit complex data where for each pair of Int8 numbers, the first
  value is real and the second is imaginary

Required Arguments:

- `file_name::String`: the data file name to parse

Returns:

- `FileInfo` struct
"""
function data_info_from_name(file_name)
	# Get file type (N-bit complex)
	if occursin("sc64", file_name)
		data_type = Val(:sc64)
	elseif occursin("sc32", file_name)
		data_type = Val(:sc32)
	elseif occursin("sc16", file_name)
		data_type = Val(:sc16)
	elseif occursin("sc8", file_name)
		data_type = Val(:sc8)
	elseif occursin("sc4", file_name)
		data_type = Val(:sc4)
	else
		error("File type not supported. Only sc4, sc8, sc16, sc32 and sc64 files are supported.")
	end
	# Determine sampling and IF frequency and frequency center
	f_s, f_if, f_center, sig_freq, sigtype = get_signal_type(file_name)
	timestamp, timestamp_JD = find_and_get_timestamp(file_name)
	return FileInfo(f_s, f_if, f_center, sig_freq, sigtype, data_type,
	                timestamp, timestamp_JD, file_name)
end

"""
	signal_to_sc_data_file(signal::GNSSSignal, sig_freq, file_location,
                           file_prefix; operation="replace")
"""
function signal_to_sc_data_file(signal::GNSSSignal, sig_freq, file_location,
	                            file_prefix; operation="replace")
    if sig_freq == L1_freq
		freq_band = "GPSL1"
	elseif sig_freq == L5_freq
		freq_band = "GPSL5"
	else
		freq_band = "$(round(sig_freq*1e-6, digits=8))"
	end
	f_center = "$(round((sig_freq - signal.f_if)*1e-6, digits=8))"  # MHz
	f_s = "$(round(signal.f_s*1e-6, digits=8))"  # MHz
	if signal.nADC == 8
		var_type = Complex{Int8}
	elseif signal.nADC == 16
		var_type = Complex{Int16}
	elseif signal.nADC == 32
		var_type = Complex{Int32}
	elseif  signal.nADC == 64
		var_type = Complex{Int64}
	else
		error("Support for 8-, 16-, 32-, and 64-bit signals only.")
	end
	file_extension = ".sc$(signal.nADC)"
	file_name = string(file_location, file_prefix, "_", freq_band, "_",
	                   f_center, "_", f_s, file_extension)
	if operation == "replace"
		file = open(file_name, "w")
	elseif operation == "append"
		file = open(file_name, "r+")
		seekend(file)
	else
		error("Unknown value for argument `operation`.")
	end
	write(file, var_type.(signal.data))
	close(file)
	return file_name
end


"""
    scan_x(scanner, x_range, x_num_scans; kwargs..)
    scan_y(scanner, y_range, y_num_scans; kwargs..)
    scan_z(scanner, z_range, z_num_scans; kwargs..)

Move along a single axis and grab data based on a fixed increment

# Arguments
- `scanner::IntensityScan`: the structure for all controller connections.
- `x_range`: An array or tuple that contains the start and end
             point of the desired range in the x-direction.
             (Must be 1 or 2 elements long)
- `x_num_scans::Int`: The number of points/scans the user desires
                      to record along the x-axis.

# Keywords
- `verbose`::Bool: Optional, defaults to: true

# Returns
- `ScanData`

# Example
```
ps = initialize(PositionerSystem)
scope = initialize(AgilentDSOX4034A)

scanner = IntensityScan(
    xyz = ps,
    scope = scope,
    channel = 1,
    post_move_delay = 0ms,
    precapture_delay = 0µs,
)

wave_x = scan_x(scanner, [-10mm, 10mm], 5)
```
"""
function scan_x(scanner::IntensityScan, x_range, num_points; verbose=true)
    scan_data = scan_single_axis(scanner, x_range, num_points, move_x_abs; verbose=verbose)
    return scan_data
end

function scan_y(scanner, y_range, num_points; verbose=true)
    scan_data = scan_single_axis(scanner, y_range, num_points, move_y_abs; verbose=verbose)
    return scan_data
end

function scan_z(scanner, z_range, num_points; verbose=true)
    scan_data = scan_single_axis(scanner, z_range, num_points, move_z_abs; verbose=verbose)
    return scan_data
end

function scan_single_axis(scanner, axis_range, num_scans, move_func; verbose=true)
    axis = get_axis(move_func)
    check_xyz_limits(scanner, axis, axis_range)
    positions = create_positions_vector(axis_range, num_scans)

    # construct the ScanData struct
    scope_info = TcpInstruments.scope_waveform_info_get(scanner.scope, scanner.channel)
    dummy_data = TcpInstruments.scope_parse_raw_waveform(zeros(scope_info.num_points), scope_info)
    time_points = dummy_data.time
    scan_data = ScanData(axis, scope_info, time_points, scope_info.num_points, [num_scans])

    pm = Progress(num_scans; desc="Scanning $axis-direction: ", enabled=verbose)
    for scan_index in 1:num_scans
        move_func(scanner.xyz, positions[scan_index])
        sleep(raw(scanner.post_move_delay))
        ProgressMeter.update!(pm)

        scanner.trigger_function()

        sleep(raw(scanner.precapture_delay))
        data = get_data(scanner.scope, scanner.channel)
        scan_data.waveform[:, scan_index] = data.volt
        scan_data.coordinates[:, scan_index] = get_pos(scanner.xyz)

        ProgressMeter.next!(pm)
    end

    return scan_data
end

function create_positions_vector(axis_range, num_points)
    if length(axis_range) == 1
        if num_points != 1
            error(
                "Can't measure multiple points when axis_range has one position"
            )
        end
        return axis_range
    elseif length(axis_range) == 2
        return range(axis_range[begin], stop=axis_range[end], length=num_points)
    else
        error("axis_range must have 1 or 2 elements")
    end
end

function check_xyz_limits(
    scanner::IntensityScan, axis::AbstractString, axis_range::Array
)
    if axis == "x"
        check_xyz_limits(get_limits_x(scanner.xyz), axis_range)
    elseif axis == "y"
        check_xyz_limits(get_limits_y(scanner.xyz), axis_range)
    elseif axis == "z"
        check_xyz_limits(get_limits_z(scanner.xyz), axis_range)
    else
        error("Single axis must be x, y or z not: $axis")
    end
end

function check_xyz_limits(limits, axis_range::Array)
    low_limit, high_limit = limits
    for range in axis_range
        if range < low_limit || range > high_limit
            error("Scan range ($range) is outside current limits of the xyz stage ($low_limit - $high_limit)")
        end
    end
end

get_axis(move_func::Function) = split(string(move_func), '_')[2]


# Property: a thing that changes how other things are drawn.

import Base.fill

export stroke, fill, linewidth, font, fontsize, visible, clip, opacity, svgid,
       svgclass, svglink, onactive, onclick, onfocusin, onfocusout, onload,
       onmousedown, onmousemove, onmouseout, onmouseover, onmouseup, svgmask,
       svgdefmask, svgembed, svgattribute, d3embed, d3hook

# A property primitive is something can be directly applied.
abstract PropertyPrimitive

# A property is a (possibly empty) sequence of property primitives. The set of
# Properties with the combine operator forms a monoid (specifically, it can be
# thought of as a free monoid). The empty_property constant is the identity
# element.
abstract Property

# An empty (i.e., nop) property which form the identity element of the Property
# monoid.
type EmptyProperty <: Property end
const empty_property = EmptyProperty()

copy(p::EmptyProperty) = p

# A non-empty sequence of property primitives.
type PropertySeq <: Property
    primitive::PropertyPrimitive
    next::Property

    function PropertySeq(primitive::PropertyPrimitive,
                         next::Property)
        new(primitive, next)
    end

    function PropertySeq(primitive::PropertyPrimitive)
        new(primitive, empty_property)
    end

    # shallow copy constructor
    function PropertySeq(ps::PropertySeq)
        new(ps.primitive, ps.next)
    end
end

copy(ps::PropertySeq) = PropertySeq(ps)

function length(p::Property)
    n = 0
    while !is(p, empty_property)
        n += 1
        p = p.next
    end
    n
end

# Combination of properties, which is simply list concatenation.
function combine(head::Property, rest::Property...)
    a = b = head === empty_property ? head : copy(head)
    for p in rest
        if p === empty_property
            continue
        elseif a === empty_property
            a = b = copy(p)
        else
            while !is(b.next, empty_property)
                b.next = copy(b.next)
                b = b.next
            end
            b.next = p
        end
    end
    a
end

combine(head::Property, ::Nothing) = head

# Unit conversion functions.

function native_measure(property::EmptyProperty,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    property
end

# Convert a sequence of properties to a sequence of properties in native
# coordinates.
function native_measure(property::PropertySeq,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    p = property = copy(property)
    while !is(p, empty_property)
        p.primitive = native_measure(p.primitive, t, unit_box, box, backend)
        p.next = copy(p.next)
        p = p.next
    end
    property
end

# Catchall for properties that don't require unit conversion.
function native_measure(property::PropertyPrimitive,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    property
end

# A property primitive controlling the fill color (or lack of color) of a form.
type Fill <: PropertyPrimitive
    value::ColorOrNothing

    Fill(value::ColorOrNothing) = new(value)
    Fill(value::String) = new(color(value))
end

# Singleton sequence contructor.
fill(value) = PropertySeq(Fill(value))

# A property primitive controlling the strok color (or lack of color) of a form.
type Stroke <: PropertyPrimitive
    value::ColorOrNothing

    Stroke(value::ColorOrNothing) = new(value)
    Stroke(value::String) = new(color(value))
end

# Singleton sequence contructor.
stroke(value) = PropertySeq(Stroke(value))

# A property primitive controlling the widths of lines drawn in stroke
# operations.
type LineWidth <: PropertyPrimitive
    value::Measure

    LineWidth(value::MeasureOrNumber) = new(size_measure(value))
end

# Singleton sequence contructor.
linewidth(value::MeasureOrNumber) = PropertySeq(LineWidth(value))

# Unit conversion
function native_measure(property::LineWidth,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    LineWidth(native_measure(property.value, t, unit_box, box, backend))
end

# Set the visible attribute in SVG output, or just skip rendering in raster
# graphics.
type Visible <: PropertyPrimitive
    value::Bool
end

visible(value::Bool) = PropertySeq(Visible(value))

type Opacity <: PropertyPrimitive
    value::Float64
end

opacity(value::Number) = PropertySeq(Opacity(convert(Float64, value)))

# Clipping path
type Clip <: PropertyPrimitive
    points::Vector{Point}
end

function clip(points::XYTupleOrPoint...)
    PropertySeq(Clip([convert(Point, point) for point in points]))
end

function native_measure(property::Clip,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    Clip([native_measure(point, t, unit_box, box, backend)
          for point in property.points])
end

# A property primitive assigning an ID, in particular in SVG, to enable
# manipulation of portions of the graphic.
type SVGID <: PropertyPrimitive
    value::String
end

# Singleton sequence contructor.
svgid(value::String) = PropertySeq(SVGID(html_escape_string(value)))

# A property setting the class field in a form's group element.
type SVGClass <: PropertyPrimitive
    value::String
end

# Singleton sequence contructor.
svgclass(value::String) = PropertySeq(SVGClass(html_escape_string(value)))

# A SVG <a> tag.
type SVGLink <: PropertyPrimitive
    target::String
end

# Singleton sequence contructor.
svglink(target::String) = PropertySeq(SVGLink(target))

# The font property primitive.
type Font <: PropertyPrimitive
    family::String
end

# Singletone sequence constructor.
font(family::String) = PropertySeq(Font(family))

# The font size property.
type FontSize <: PropertyPrimitive
    value::Measure

    FontSize(value::MeasureOrNumber) = new(size_measure(value))
end

# Singletone sequence constructor.
fontsize(value::MeasureOrNumber) = PropertySeq(FontSize(value))

# Native unit conversion.
function native_measure(property::FontSize,
                        t::NativeTransform,
                        unit_box::BoundingBox,
                        box::NativeBoundingBox,
                        backend::Backend)
    FontSize(native_measure(property.value, t, unit_box, box, backend))
end

# Events:
# These correspond to DOM events, and associate some javascript blurb that gets
# executed when the event is triggered.

const events = (:OnActivate, :OnClick, :OnFocusIn, :OnFocusOut,
                :OnLoad, :OnMouseDown, :OnMouseMove, :OnMouseOut,
                :OnMouseOver, :OnMouseUp)

for event in events
    event_lc  = symbol(lowercase(string(event)))
    @eval begin
        type ($event) <: PropertyPrimitive
            value::String
        end

        function ($event_lc)(value::String)
            PropertySeq(($event)(value))
        end
    end
end

# SVG Mask objects

type SVGMask <: PropertyPrimitive
    id::String
end

svgmask(id::String) = PropertySeq(SVGMask(id))

type SVGDefineMask <: PropertyPrimitive
    id::String
end

svgdefmask(id::String) = PropertySeq(SVGDefineMask(id))

# A general purpose SVG attribute.

type SVGAttribute <: PropertyPrimitive
    attribute::String
    value::String
end

function svgattribute(attribute::String, value::String)
    PropertySeq(SVGAttribute(attribute, value))
end

# Embedding raw markup in SVG files.

# This is a property that when processed will be saved and then appended to the
# end of the SVG file, but ignored by default on other backends. This allows us
# to use some of SVGs more obsure features without exposing all of SVG.

type SVGEmbed <: PropertyPrimitive
    markup::String
end

svgembed(markup::String) = PropertySeq(SVGEmbed(markup))

# Embedding raw js code in D3 output.

type D3Embed <: PropertyPrimitive
    code::String
end

type D3Hook <: PropertyPrimitive
    code::String
end

d3hook(code::String) = PropertySeq(D3Hook(code))


############################################################################################
# Assign abstract types
"""
$(TYPEDEF)
Abstract super type for Baytes Models.
"""
abstract type ModelName end
"""
$(TYPEDEF)
Default modelname of Baytes.Model struct.
"""
struct BaseModel <: ModelName end

############################################################################################
#!NOTE: For M<:Union{P,ModelName} where {P}, keep A so option using of other PPLs is easily configurable.
"""
$(TYPEDEF)
Baytes Model struct.

Contains information about current Model value, name, and information, see also [`ParameterInfo`](@ref).

# Fields
$(TYPEDFIELDS)
"""
mutable struct ModelWrapper{
    M<:Union{P,ModelName} where {P},A<:NamedTuple,B<:ParameterInfo
} <: BaytesCore.AbstractModelWrapper
    "Current Model values as NamedTuple - works with Nested Tuples."
    val::A
    "Information about parameter distributions, transformations and constraints, see [`ParameterInfo`](@ref)."
    info::B
    "Model id, per default BaseModel. Useful for dispatching ModelWrapper struct."
    id::M
    function ModelWrapper(
        val::A, info::B, id::M
    ) where {M<:Union{P,ModelName} where {P},A<:NamedTuple,B<:ParameterInfo}
        return new{M,A,B}(val, info, id)
    end
end
# Convenient Constructor
function ModelWrapper(
    id::M, parameter::A, flattendefault::F=FlattenDefault()
) where {M<:Union{P,ModelName} where {P},A<:NamedTuple,F<:FlattenDefault}
    ## Check if all values in val are of type Param
    ArgCheck.@argcheck _checkparams(parameter) "All values in (nested) NamedTuple have to be of Type Param."
    ## Split between values and constraints
    val = _get_val(parameter)
    constraint = _get_constraint(parameter)
    ## Create ParameterInfo struct
    paraminfo = ParameterInfo(constraint, val, flattendefault)
    ## Return ModelWrapper
    return ModelWrapper(val, paraminfo, id)
end
ModelWrapper(parameter::A, flattendefault::F=FlattenDefault()) where {A<:NamedTuple,F<:FlattenDefault} =
    ModelWrapper(BaseModel(), parameter, flattendefault)

############################################################################################
# Basic functions for Model struct
length(model::ModelWrapper) = model.info.reconstruct.unflatten.strict._unflatten.sz[end]
paramnames(model::ModelWrapper) = keys(model.val)

############################################################################################
# A bunch of functions that can be used/extended for target model in Sampling process
"""
$(SIGNATURES)
Simulate data given Model parameter.

# Examples
```julia
```

"""
function simulate(model::ModelWrapper) end

############################################################################################
# Dispatch Model struct for .Core functions
"""
$(SIGNATURES)
Fill 'model' values with NamedTuple 'θ'.

# Examples
```julia
```

"""
function fill(model::ModelWrapper, θ::NamedTuple)
    return merge(model.val, θ)
end

"""
$(SIGNATURES)
Inplace version of [`fill`](@ref).

# Examples
```julia
```

"""
function fill!(model::ModelWrapper, θ::NamedTuple)
    model.val = merge(model.val, θ)
    return nothing
end

function subset(model::ModelWrapper, sym)
    return subset(model.val, sym)
end

#########################################
"""
$(SIGNATURES)
Unconstrain 'model' values and return as NamedTuple.

# Examples
```julia
```

"""
function unconstrain(model::ModelWrapper)
    return unconstrain(model.info.transform, model.val)
end

"""
$(SIGNATURES)
Flatten 'model' values and return as vector.

# Examples
```julia
```

"""
function flatten(model::ModelWrapper)
    return flatten(model.info.reconstruct, model.val)
end

"""
$(SIGNATURES)
Flatten and unconstrain 'model' values and return as vector.

# Examples
```julia
```

"""
function unconstrain_flatten(model::ModelWrapper)
    return flatten(model.info.reconstruct, unconstrain(model))
end

#########################################
"""
$(SIGNATURES)
Unlatten Vector 'θ' given constraints from 'model' and return as NamedTuple.

# Examples
```julia
```

"""
function unflatten(model::ModelWrapper, θ::AbstractVector{T}) where {T<:Real}
    return unflatten(model.info.reconstruct, θ)
end

"""
$(SIGNATURES)
Inplace version of [`unflatten`](@ref).

# Examples
```julia
```

"""
function unflatten!(model::ModelWrapper, θ::AbstractVector{T}) where {T<:Real}
    model.val = unflatten(model, θ)
    return nothing
end

"""
$(SIGNATURES)
Constrain and Unflatten vector 'θᵤ' given 'model' constraints.

# Examples
```julia
```

"""
function unflatten_constrain(model::ModelWrapper, θᵤ::AbstractVector{T}) where {T<:Real}
    return constrain(model.info.transform, unflatten(model, θᵤ))
end

"""
$(SIGNATURES)
Inplace version of [`unflatten_constrain`](@ref).

# Examples
```julia
```

"""
function unflatten_constrain!(model::ModelWrapper, θᵤ::AbstractVector{T}) where {T<:Real}
    model.val = unflatten_constrain(model, θᵤ)
    return nothing
end

#########################################
"""
$(SIGNATURES)
Sample from 'model' prior and return as NamedTuple.

# Examples
```julia
```

"""
function sample(_rng::Random.AbstractRNG, model::ModelWrapper)
    return sample_constraint(_rng, model.info.constraint, model.val)
end
sample(model::ModelWrapper) = sample(Random.GLOBAL_RNG, model)

"""
$(SIGNATURES)
Inplace version of [`sample`](@ref).

# Examples
```julia
```

"""
function sample!(_rng::Random.AbstractRNG, model::ModelWrapper)
    ArgCheck.@argcheck _checkprior(model.info.constraint) "For inplace sample version, all constraints need to be a Distribution."
    model.val = sample(_rng, model)
    return nothing
end
sample!(model::ModelWrapper) = sample!(Random.GLOBAL_RNG, model)

#########################################
"""
$(SIGNATURES)
Evaluate Log density of 'model' prior given current 'model' values.

# Examples
```julia
```

"""
function log_prior(model::ModelWrapper)
    return log_prior(model.info.constraint, model.val)
end

"""
$(SIGNATURES)
Evaluate Log density and eventual Jacobian adjustments of 'model' prior given current 'model' values.

# Examples
```julia
```

"""
function log_prior_with_transform(model::ModelWrapper)
    return log_prior_with_transform(model.info.constraint, model.val)
end

"""
$(SIGNATURES)
Evaluate eventual Jacobian adjustments from transformations at 'model' values.

# Examples
```julia
```

"""
function log_abs_det_jac(model::ModelWrapper)
    return log_abs_det_jac(model.info.transform.unconstrain, model.val)
end

############################################################################################
export
    ModelName,
    BaseModel,
    ModelWrapper,
    simulate,
    length,
    paramnames,
    fill,
    fill!,
    subset,
    flatten,
    unconstrain_flatten,
    unflatten,
    unflatten!,
    unconstrain,
    unflatten_constrain,
    unflatten_constrain!,
    sample,
    sample!,
    log_prior,
    log_prior_with_transform,
    log_abs_det_jac


module AutoOffload

using LinearAlgebra, AbstractFFTs, FFTW

@static if Base.find_package("CuArrays") !== nothing
    using CuArrays
    if Float64(CuArrays.CUDAdrv.totalmem(first(CuArrays.CUDAdrv.devices()))) > 1e9
        @info("CUDA support found, automatic GPU acceleration will be enabled.")
        const GPU_SUPPORTED = true
        const AUTO_GPU_SIZE = 100
        cuify(x) = CuArrays.CuArray(x)

        # Piracy, should get upstreamed
        function Base.ldiv!(x::CuArrays.CuArray,_qr::CuArrays.CUSOLVER.CuQR,b::CuArrays.CuArray)
          _x = UpperTriangular(_qr.R) \ (_qr.Q' * reshape(b,length(b),1))
          x .= vec(_x)
        end
    else
        @info("CUDA support not found, GPU acceleration will not be available.")
        const GPU_SUPPORTED = false
        const AUTO_GPU_SIZE = 100
        cuify(x) = x
    end
else
    @info("CUDA support not found, GPU acceleration will not be available.")
    const GPU_SUPPORTED = false
    const AUTO_GPU_SIZE = 100
    cuify(x) = x
end

function accelerated_mul!(X,A,B)
    if GPU_SUPPORTED && size(A,1) > AUTO_GPU_SIZE && b isa Array
        A,B = cuify(A),cuify(B)
        X .= Array(A*B)
    else
        mul!(X,A,B)
    end
end

function accelerated_ldiv!(X,A,B)
    if GPU_SUPPORTED && size(A,1) > AUTO_GPU_SIZE && b isa Array
        _X,A,B = cuify(X),cuify(A),cuify(B)
        X .= Array(ldiv!(_X,A,B))
    else
        ldiv!(X,A,B)
    end
end

function accelerated_factorize!(A)
    if GPU_SUPPORTED && size(A,1) > AUTO_GPU_SIZE && b isa Array
        qr(cuify(A))
    else
        factorize(A)
    end
end

function accelerated_fft!(A)
    if GPU_SUPPORTED && size(A,1) > AUTO_GPU_SIZE && b isa Array
        A .= Array(fft!(cuify(A)))
    else
        FFTW.fft!(A)
    end
end

export accelerated_mul!, accelerated_ldiv!, accelerated_factorize!,
       accelerated_fft!

module Pirate

    # Somehow take over mul!, *, etc to autooffload to GPU?

end

end # module


"""
Task is to copy content from the input tape to
the output tape. http://arxiv.org/abs/1511.07275
"""

using DataStructures: CircularBuffer
include("AlgorithmicEnv.jl")

RealOrNothing = Union{Float32, Nothing}

mutable struct CopyEnv <: TapeAlgorithmicEnv
    MIN_REWARD_SHORTFALL_FOR_PROMOTION::Float32
    base::Int8  # Number of distinct characters
    episode_total_reward::RealOrNothing  # Cumulative reward earned this episode

    # Running tally of reward shortfalls. eg If there were 10 points to earn
    # and we got 8, we'd append -2
    reward_shortfalls::CircularBuffer{Float32}

    charmap::Array{Char, 1}  # Array of all the characters used while rendering
    min_length::Int8  # Minimum length of the input tape
    action_space::TupleSpace
    observation_space::Discrete

    MOVEMENTS::NTuple # Possible directions to move in.
    READ_HEAD_START::Int8  # Starting position of the read head

    target::Array{N, 1} where N <: Union{Char, Integer}  # target tape
    input_data::Array{N, 1} where N <: Union{Char, Integer}
    time::Int8
    read_head_position::Int8  # current position of the read head.
    write_head_position::Int8
    last_action
    last_reward::Float32
end

function CopyEnv(base::Int=5, chars::Bool=true)
    starting_char = chars ? 'A' : '0'
    CopyEnv(-1f0, # MIN_REWARD_SHORTFALL_FOR_PROMOTION
            base,
            0f0, # episode_total_reward
            CircularBuffer{Float32}(10),  # reward_shortfalls
            push!([starting_char+i for i=0:base-1], ' '),  # charmap
            2,  # starting min_length
            TupleSpace([Discrete(2), Discrete(2), Discrete(base)]),  # action_space
            Discrete(base+1),  # observation_space
            (:left, :right), 1,  # MOVEMENTS and READ_HEAD_START
            Array{(chars ? Char : Int8), 1}(), # target
            Array{(chars ? Char : Int8), 1}(), # input_data
            0,  # time
            1,  # read_head_posiion
            1,  # write_head_position
            nothing,  # last_action
            0.0)  # last_reward
end

target_from_input_data!(env::CopyEnv) = env.target = env.input_data


function LToggle(parent::Scene; bbox = nothing, kwargs...)
    attrs = merge!(Attributes(kwargs), default_attributes(LToggle))

    @extract attrs (halign, valign, cornersegments, framecolor_inactive,
        framecolor_active, buttoncolor, active, toggleduration, rimfraction)

    decorations = Dict{Symbol, Any}()

    sizeattrs = sizenode!(attrs.width, attrs.height)
    alignment = lift(tuple, halign, valign)

    suggestedbbox = create_suggested_bboxnode(bbox)

    autosizenode = Node{NTuple{2, Optional{Float32}}}((nothing, nothing))

    computedsize = computedsizenode!(sizeattrs, autosizenode)

    finalbbox = alignedbboxnode!(suggestedbbox, computedsize, alignment, sizeattrs, autosizenode)

    markersize = lift(finalbbox) do bbox
        min(width(bbox), height(bbox))
    end

    button_endpoint_inactive = lift(markersize) do ms
        bbox = finalbbox[]
        Point2f0(left(bbox) + ms / 2, bottom(bbox) + ms / 2)
    end

    button_endpoint_active = lift(markersize) do ms
        bbox = finalbbox[]
        Point2f0(right(bbox) - ms / 2, bottom(bbox) + ms / 2)
    end

    buttonvertices = lift(markersize, cornersegments) do ms, cs
        roundedrectvertices(finalbbox[], ms * 0.499, cs)
    end

    # trigger bbox
    suggestedbbox[] = suggestedbbox[]

    framecolor = Node{Any}(framecolor_inactive[])
    frame = poly!(parent, buttonvertices, color = framecolor, raw = true)[end]

    animating = Node(false)
    buttonpos = Node(active[] ? [button_endpoint_active[]] : [button_endpoint_inactive[]])

    # make the button stay in the correct place (and start there)
    on(finalbbox) do bbox
        if !animating[]
            buttonpos[] = active[] ? [button_endpoint_active[]] : [button_endpoint_inactive[]]
        end
    end

    buttonsize = lift(markersize, rimfraction) do ms, rf
        ms * (1 - rf)
    end

    button = scatter!(parent, buttonpos, markersize = buttonsize, color = buttoncolor, raw = true)[end]

    buttonstate = addmousestate!(parent, button, frame)

    onmouseleftclick(buttonstate) do state
        if animating[]
            return
        end
        animating[] = true

        tstart = time()

        anim_posfrac = Animations.Animation(
            [0, toggleduration[]],
            active[] ? [1.0, 0.0] : [0.0, 1.0],
            Animations.sineio())
        coloranim = Animations.Animation(
            [0, toggleduration[]],
            active[] ? [framecolor_active[], framecolor_inactive[]] : [framecolor_inactive[], framecolor_active[]],
            Animations.sineio())

        active[] = !active[]
        @async while true
            t = time() - tstart
            # request endpoint values in every frame if the layout changes during
            # the animation
            buttonpos[] = [Animations.linear_interpolate(anim_posfrac(t),
                button_endpoint_inactive[], button_endpoint_active[])]
            framecolor[] = coloranim(t)
            if t >= toggleduration[]
                animating[] = false
                break
            end
            sleep(1/FPS[])
        end
    end

    # no protrusions
    protrusions = Node(RectSides(0f0, 0f0, 0f0, 0f0))

    layoutnodes = LayoutNodes{LToggle, GridLayout}(suggestedbbox, protrusions, computedsize, autosizenode, finalbbox, nothing)

    LToggle(parent, layoutnodes, attrs, decorations)
end

defaultlayout(lt::LToggle) = ProtrusionLayout(lt)

computedsizenode(lt::LToggle) = lt.layoutnodes.computedsize
protrusionnode(lt::LToggle) = lt.layoutnodes.protrusions

function align_to_bbox!(lt::LToggle, bbox)
    lt.layoutnodes.suggestedbbox[] = bbox
end

function Base.getproperty(lt::LToggle, s::Symbol)
    if s in fieldnames(LToggle)
        getfield(lt, s)
    else
        lt.attributes[s]
    end
end

function Base.setproperty!(lt::LToggle, s::Symbol, value)
    if s in fieldnames(LToggle)
        setfield!(lt, s, value)
    else
        lt.attributes[s][] = value
    end
end

function Base.propertynames(lt::LToggle)
    [fieldnames(LToggle)..., keys(lt.attributes)...]
end


println("Ab) Read in a full multiplet representation from files compatible to Ratip2012 and Grasp2018.")
#
wa = JAC.ManyElectron.Multiplet("from Ratip2012", "../test/files/ratip2012-at-is-a1-csl.inp", "../test/files/ratip2012-at-is-a1-scf.out",
                                                  "../test/files/ratip2012-at-is-a1-relci.mix")

println("Not yet implemented; files do not exist !")
wa = JAC.ManyElectron.Multiplet("from Grasp2018", "../test/files/grasp2018-at-is-a1-csl.inp", "../test/files/grasp2018-at-is-a1-scf.out",
                                                  "../test/files/grasp2018-at-is-a1-rci.mix")