using FlightMechanics
using FlightMechanics.Models

##----------------------------------------------------------------------------------------------------
## imports

# Aerodynamics
import FlightMechanics.Models:
    calculate_aerodynamics

# Propulsion
import FlightMechanics.Models:
    get_pfm, get_cj, get_power, get_efficiency, get_tanks,
    get_engine_position, get_engine_orientation, get_engine_gyro_effects,
    calculate_engine

# Aircraft Model
import FlightMechanics.Models:
    get_name, get_wing_area, get_wing_span, get_chord, get_arp,
    get_empty_mass_props,
    set_stick_lon!, set_stick_lat!, set_pedals!, set_thtl!,
    get_controls_ranges_trimmer

##----------------------------------------------------------------------------------------------------
## exports

# Aerodynamics
export F16Aerodynamics, calculate_aerodynamics

# Propulsion
export F16Engine,
    get_pfm, get_cj, get_power, get_efficiency, get_tanks,
    get_engine_position, get_engine_orientation,
    calculate_engine

# FCS
export F16FCS

# Aircraft Model
export F16,
    get_name,
    get_empty_mass_props,
    get_wing_area,
    get_wing_span,
    get_chord,
    get_arp

##----------------------------------------------------------------------------------------------------
## structs

# Aerodynamics
struct F16Aerodynamics<:Aerodynamics    # TODO: CDα, CLα, CYβ... could be included here
    wind_pfm::PointForcesMoments
    wind_coeff_pfm::PointForcesMoments
    body_pfm::PointForcesMoments
    body_coeff_pfm::PointForcesMoments
end

# Propulsion
struct F16Engine<:Engine
    pfm::PointForcesMoments
    cj::Number
    power::Number
    efficiency::Number
    tanks::Array{RigidSolid, 1}
    h::Array{T, 1} where T<:Number  # angular momentum [kg·m²/s]
end

# FCS
mutable struct F16FCS<:FCS      # TODO: implement also rate limits and time constants
    # Cabin controls
    stick_longitudinal::RangeControl
    stick_lateral::RangeControl
    pedals::RangeControl
    thtl::RangeControl

    # Aerodynamic surfaces
    de::RangeControl
    da::RangeControl
    dr::RangeControl
    # Engine
    cpl::RangeControl
    # Configuration
    allow_out_of_range::Bool
    throw_error_on_out_of_range::Bool
end

# Aircraft Model
struct F16<:Aircraft
    mass_props::RigidSolid
    pfm::PointForcesMoments
    aerodynamics::F16Aerodynamics
    propulsion::Propulsion
    fcs::FCS
end

##----------------------------------------------------------------------------------------------------
## constants

const DE_MAX = 25.0  # deg
const DA_MAX = 20.0  # deg  #XXX: In Stevens' book says 21.5 deg (Appendix A Section A.4)
const DR_MAX = 30.0  # deg

##----------------------------------------------------------------------------------------------------
## Aerodynamics

α_data = [-10. -5. 0. 5. 10. 15. 20. 25. 30. 35. 40. 45.]  # deg
β_data = [0. 5. 10. 15. 20. 25. 30.]  # deg

de_data = [-24. -12.   0.  12.  24.]  # deg

# UTILS for interpolations
function get_interp_idx(val, coeff, min_, max_, off_=3)
   s = val * coeff
   k = floor(Int, s)
   k = max(min_, k)
   k = min(k, max_)
   da = s - float(k)
   k += off_
   l = k + Int(sign(da))
   return k, l, da
end

function get_interp_idx2(val, coeff, min_, max_)
   s = abs(val) * coeff
   k = floor(Int, s)
   k = max(min_, k)
   k = min(k, max_)
   da = s - float(k)
   k += 1
   l = k + Int(sign(da))
   return k, l, da
end

function interp1d(val, coeff, min, max, data)
   k, l, da = get_interp_idx(val, coeff, min, max)
   res = data[k] + abs(da) * (data[l] - data[k])
   return res
end

function interp2d(val1, val2, coeff1, coeff2, min1, min2, max1, max2, data, off1=3, off2=3)
   k, l, da = get_interp_idx(val1, coeff1, min1, max1, off1)
   m, n, de = get_interp_idx(val2, coeff2, min2, max2, off2)

   t = data[k, m]
   u = data[k, n]
   v = t + abs(da) * (data[l, m] - t)
   w = u + abs(da) * (data[l, n] - u)
   res = v + (w - v) * abs(de)

   return res
end

function interp2d2(val1, val2, coeff1, coeff2, min1, min2, max1, max2, data)
   k, l, da = get_interp_idx(val1, coeff1, min1, max1)
   m, n, de = get_interp_idx2(val2, coeff2, min2, max2)

   t = data[k, m]
   u = data[k, n]
   v = t + abs(da) * (data[l, m] - t)
   w = u + abs(da) * (data[l, n] - u)
   res = v + (w - v) * abs(de)
   res *= sign(val2)

   return res
end

# DAMPING COEFFICIENTS
CXq_data = [-0.267  -0.110   0.308   1.340   2.080   2.910   2.760   2.050   1.500   1.490   1.830   1.210]
CYr_data = [ 0.882   0.852   0.876   0.958   0.962   0.974   0.819   0.483   0.590   1.210  -0.493  -1.040]
# XXX: -2.27 seems an error in Stenvens. In Morelli it is -0.2270
# CYp_data = [-0.108  -0.108  -0.188   0.110   0.258   0.226   0.344   0.362   0.611   0.529   0.298  -2.270]
CYp_data = [-0.108  -0.108  -0.188   0.110   0.258   0.226   0.344   0.362   0.611   0.529   0.298  -0.227]
CZq_data = [-8.800 -25.800 -28.900 -31.400 -31.200 -30.700 -27.700 -28.200 -29.000 -29.800 -38.300 -35.300]
Clr_data = [-0.126  -0.026   0.063   0.113   0.208   0.230   0.319   0.437   0.680   0.100   0.447  -0.330]
Clp_data = [-0.360  -0.359  -0.443  -0.420  -0.383  -0.375  -0.329  -0.294  -0.230  -0.210  -0.120  -0.100]
Cmq_data = [-7.210  -0.540  -5.230  -5.260  -6.110  -6.640  -5.690  -6.000  -6.200  -6.400  -6.600  -6.000]
Cnr_data = [-0.380  -0.363  -0.378  -0.386  -0.370  -0.453  -0.550  -0.582  -0.595  -0.637  -1.020  -0.840]
Cnp_data = [ 0.061   0.052   0.052  -0.012  -0.013  -0.024   0.050   0.150   0.130   0.158   0.240   0.150]

damp_data = [CXq_data; CYr_data; CYp_data; CZq_data; Clr_data; Clp_data;
             Cmq_data; Cnr_data; Cnp_data]'
damp_data = damp_data'

# CX
# rows: de, columns: alpha
CX_data = [-0.099 -0.081 -0.081 -0.063 -0.025 0.044 0.097 0.113 0.145 0.167 0.174 0.166;
           -0.048 -0.038 -0.040 -0.021  0.016 0.083 0.127 0.137 0.162 0.177 0.179 0.167;
           -0.022 -0.020 -0.021 -0.004  0.032 0.094 0.128 0.130 0.154 0.161 0.155 0.138;
           -0.040 -0.038 -0.039 -0.025  0.006 0.062 0.087 0.085 0.100 0.110 0.104 0.091;
           -0.083 -0.073 -0.076 -0.072 -0.046 0.012 0.024 0.025 0.043 0.053 0.047 0.040]
CX_data = CX_data'

# CZ
# columns: alpha
CZ_data = [0.770 0.241 -0.100 -0.416 -0.731 -1.053 -1.366 -1.646 -1.917 -2.120 -2.248 -2.229]

# Cm
# rows: de columns: alpha
Cm_data = [0.205  0.168  0.186  0.196  0.213  0.251  0.245  0.238  0.252  0.231  0.198  0.192;
           0.081  0.077  0.107  0.110  0.110  0.141  0.127  0.119  0.133  0.108  0.081  0.093;
          -0.046 -0.020 -0.009 -0.005 -0.006  0.010  0.006 -0.001  0.014  0.000 -0.013  0.032;
          -0.174 -0.145 -0.121 -0.127 -0.129 -0.102 -0.097 -0.113 -0.087 -0.084 -0.069 -0.006;
          -0.259 -0.202 -0.184 -0.193 -0.199 -0.150 -0.160 -0.167 -0.104 -0.076 -0.041 -0.005]
Cm_data = Cm_data'

# Cl
# rows: beta columns: alpha
# XXX: found differences in Morelli Cl_data for columns 6-9
# Stevens
Cl_data = [ 0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000;
           -0.001 -0.004 -0.008 -0.012 -0.016 -0.019 -0.020 -0.020 -0.015 -0.008 -0.013 -0.015;
           -0.003 -0.009 -0.017 -0.024 -0.030 -0.034 -0.040 -0.037 -0.016 -0.002 -0.010 -0.019;
           -0.001 -0.010 -0.020 -0.030 -0.039 -0.044 -0.050 -0.049 -0.023 -0.006 -0.014 -0.027;
            0.000 -0.010 -0.022 -0.034 -0.047 -0.046 -0.059 -0.061 -0.033 -0.036 -0.035 -0.035;
            0.007 -0.010 -0.023 -0.034 -0.049 -0.046 -0.068 -0.071 -0.060 -0.058 -0.062 -0.059;
            0.009 -0.011 -0.023 -0.037 -0.050 -0.047 -0.074 -0.079 -0.091 -0.076 -0.077 -0.076]
Cl_data = Cl_data'

# Morelli version
# Cl_data = [ 0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000;
#            -0.001 -0.004 -0.008 -0.012 -0.016 -0.022 -0.022 -0.021 -0.015 -0.008 -0.013 -0.015;
#            -0.003 -0.009 -0.017 -0.024 -0.030 -0.041 -0.045 -0.040 -0.016 -0.002 -0.010 -0.019;
#            -0.001 -0.010 -0.020 -0.030 -0.039 -0.054 -0.057 -0.054 -0.023 -0.006 -0.014 -0.027;
#             0.000 -0.010 -0.022 -0.034 -0.047 -0.060 -0.069 -0.067 -0.033 -0.036 -0.035 -0.035;
#             0.007 -0.010 -0.023 -0.034 -0.049 -0.063 -0.081 -0.079 -0.060 -0.058 -0.062 -0.059;
#             0.009 -0.011 -0.023 -0.037 -0.050 -0.068 -0.089 -0.088 -0.091 -0.076 -0.077 -0.076]
# Cl_data = Cl_data'

# rows: beta columns=alpha
clda_data = [-0.041 -0.052 -0.053 -0.056 -0.050 -0.056 -0.082 -0.059 -0.042 -0.038 -0.027 -0.017;
             -0.041 -0.053 -0.053 -0.053 -0.050 -0.051 -0.066 -0.043 -0.038 -0.027 -0.023 -0.016;
             -0.042 -0.053 -0.052 -0.051 -0.049 -0.049 -0.043 -0.035 -0.026 -0.016 -0.018 -0.014;
             -0.040 -0.052 -0.051 -0.052 -0.048 -0.048 -0.042 -0.037 -0.031 -0.026 -0.017 -0.012;
             -0.043 -0.049 -0.048 -0.049 -0.043 -0.042 -0.042 -0.036 -0.025 -0.021 -0.016 -0.011;
             -0.044 -0.048 -0.048 -0.047 -0.042 -0.041 -0.020 -0.028 -0.013 -0.014 -0.011 -0.010;
             -0.043 -0.049 -0.047 -0.045 -0.042 -0.037 -0.003 -0.013 -0.010 -0.003 -0.007 -0.008]
clda_data /= DA_MAX
clda_data = clda_data'
# Stevens
# rows: beta columns=alpha
cldr_data = [ 0.005  0.017  0.014  0.010 -0.005  0.009  0.019  0.005 -0.000 -0.005 -0.011  0.008;
              0.007  0.016  0.014  0.014  0.013  0.009  0.012  0.005  0.000  0.004  0.009  0.007;
              0.013  0.013  0.011  0.012  0.011  0.009  0.008  0.005 -0.002  0.005  0.003  0.005;  # Morelli diff
              0.018  0.015  0.015  0.014  0.014  0.014  0.014  0.015  0.013  0.011  0.006  0.001;
              0.015  0.014  0.013  0.013  0.012  0.011  0.011  0.010  0.008  0.008  0.007  0.003;
              0.021  0.011  0.010  0.011  0.010  0.009  0.008  0.010  0.006  0.005  0.000  0.001;
              0.023  0.010  0.011  0.011  0.011  0.010  0.008  0.010  0.006  0.014  0.020  0.000]
cldr_data /= DR_MAX
cldr_data = cldr_data'

# Morelli
# rows: beta columns=alpha
# cldr_data = [ 0.005  0.017  0.014  0.010 -0.005  0.009  0.019  0.005 -0.000 -0.005 -0.011  0.008;
#               0.007  0.016  0.014  0.014  0.013  0.009  0.012  0.005  0.000  0.004  0.009  0.007;
#               0.013  0.013  0.011  0.012  0.011  0.009  0.008  0.005  0.000  0.005  0.003  0.005;  # <<
#               0.018  0.015  0.015  0.014  0.014  0.014  0.014  0.015  0.013  0.011  0.006  0.001;
#               0.015  0.014  0.013  0.013  0.012  0.011  0.011  0.010  0.008  0.008  0.007  0.003;
#               0.021  0.011  0.010  0.011  0.010  0.009  0.008  0.010  0.006  0.005  0.000  0.001;
#               0.023  0.010  0.011  0.011  0.011  0.010  0.008  0.010  0.006  0.014  0.020  0.000]
# cldr_data /= DR_MAX
# cldr_data = cldr_data'

# Cn
# rows: beta columns: alpha
Cn_data = [0.000  0.000  0.000  0.000  0.000  0.000  0.000  0.000 0.000  0.000  0.000  0.000;
           0.018  0.019  0.018  0.019  0.019  0.018  0.013  0.007 0.004 -0.014 -0.017 -0.033;
           0.038  0.042  0.042  0.042  0.043  0.039  0.030  0.017 0.004 -0.035 -0.047 -0.057;
           0.056  0.057  0.059  0.058  0.058  0.053  0.032  0.012 0.002 -0.046 -0.071 -0.073;
           0.064  0.077  0.076  0.074  0.073  0.057  0.029  0.007 0.012 -0.034 -0.065 -0.041;
           0.074  0.086  0.093  0.089  0.080  0.062  0.049  0.022 0.028 -0.012 -0.002 -0.013;
           0.079  0.090  0.106  0.106  0.096  0.080  0.068  0.030 0.064  0.015  0.011 -0.001]
Cn_data = Cn_data'
# Stevens
# rows: beta columns=alpha
cnda_data = [ 0.001 -0.027 -0.017 -0.013 -0.012 -0.016  0.001  0.017  0.011  0.017  0.008  0.016;
              0.002 -0.014 -0.016 -0.016 -0.014 -0.019 -0.021  0.002  0.012  0.015  0.015  0.011;
             -0.006 -0.008 -0.006 -0.006 -0.005 -0.008 -0.005  0.007  0.004  0.007  0.006  0.006;
             -0.011 -0.011 -0.010 -0.009 -0.008 -0.006  0.000  0.004  0.007  0.010  0.004  0.010;
             -0.015 -0.015 -0.014 -0.012 -0.011 -0.008 -0.002  0.002  0.006  0.012  0.011  0.011;
             -0.024 -0.010 -0.004 -0.002 -0.001  0.003  0.014  0.006 -0.001  0.004  0.004  0.006;
             -0.022  0.002 -0.003 -0.005 -0.003 -0.001 -0.009 -0.009 -0.001  0.003 -0.002  0.001]
cnda_data /= DA_MAX
cnda_data = cnda_data'

# Morelli
# rows: beta columns=alpha
# cnda_data = [ 0.001 -0.027 -0.017 -0.013 -0.012 -0.016  0.001  0.017  0.011  0.017  0.008  0.016;
#               0.002 -0.014 -0.016 -0.016 -0.014 -0.019 -0.021  0.002  0.012  0.015  0.016  0.011; # <<
#              -0.006 -0.008 -0.006 -0.006 -0.005 -0.008 -0.005  0.007  0.004  0.007  0.006  0.006;
#              -0.011 -0.011 -0.010 -0.009 -0.008 -0.006  0.000  0.004  0.007  0.010  0.004  0.010;
#              -0.015 -0.015 -0.014 -0.012 -0.011 -0.008 -0.002  0.002  0.006  0.012  0.011  0.011;
#              -0.024 -0.010 -0.004 -0.002 -0.001  0.003  0.014  0.006 -0.001  0.004  0.004  0.006;
#              -0.022  0.002 -0.003 -0.005 -0.003 -0.001 -0.009 -0.009 -0.001  0.003 -0.002  0.001]
# cnda_data /= DA_MAX
# cnda_data = cnda_data'

# rows: beta columns=alpha
cndr_data = [-0.018 -0.052 -0.052 -0.052 -0.054 -0.049 -0.059 -0.051 -0.030 -0.037 -0.026 -0.013;
             -0.028 -0.051 -0.043 -0.046 -0.045 -0.049 -0.057 -0.052 -0.030 -0.033 -0.030 -0.008;
             -0.037 -0.041 -0.038 -0.040 -0.040 -0.038 -0.037 -0.030 -0.027 -0.024 -0.019 -0.013;
             -0.048 -0.045 -0.045 -0.045 -0.044 -0.045 -0.047 -0.048 -0.049 -0.045 -0.033 -0.016;
             -0.043 -0.044 -0.041 -0.041 -0.040 -0.038 -0.034 -0.035 -0.035 -0.029 -0.022 -0.009;
             -0.052 -0.034 -0.036 -0.036 -0.035 -0.028 -0.024 -0.023 -0.020 -0.016 -0.010 -0.014;
             -0.062 -0.034 -0.027 -0.028 -0.027 -0.027 -0.023 -0.023 -0.019 -0.009 -0.025 -0.010]
cndr_data /= DR_MAX
cndr_data = cndr_data'

"""
   dampings(aero::F16Aerodynamics, α)

Calculates CX_q, CY_r, CY_p, CZ_q, Cl_r, Cl_p, Cm_q, Cn_r, Cn_p for the given
angle of attack, α (deg).
"""
function dampings(aero::F16Aerodynamics, α)
   # k, l, da = get_interp_idx(α, 0.2, -1, 8)
   # dampings = damp_data[k, :] + abs(da) * (damp_data[l, :] - damp_data[k, :])
   # TODO: check if vectorized approach is faster than loop (it shouldn't)
   dampings = Array{Float64}(undef, 9)
   for ii in 1:9
      dampings[ii] = interp1d(α, 0.2, -1, 8, damp_data[ii, :])
   end
   return dampings
end

Cl_da(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, clda_data, 3, 4)
Cl_dr(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cldr_data, 3, 4)
Cn_da(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cnda_data, 3, 4)
Cn_dr(aero::F16Aerodynamics, α, β) = interp2d(α, β, 0.2, 0.1, -1, -2, 8, 2, cndr_data, 3, 4)

CXαde(aero::F16Aerodynamics, α, de) = interp2d(α, de, .2, 1.0/12, -1, -1, 8, 1, CX_data)

CYdr(aero::F16Aerodynamics, dr) = 0.086 / DR_MAX * dr
CYda(aero::F16Aerodynamics, da) = 0.021 / DA_MAX * da
CYβ(aero::F16Aerodynamics, β) = -0.02 * β

CZα(aero::F16Aerodynamics, α) = interp1d(α, 0.2, -1, 8, CZ_data)
CZαβ(aero::F16Aerodynamics, α, β) = CZα(aero, α) * (1. - (β*DEG2RAD)^2)
CZde(aero::F16Aerodynamics, de) = -0.19/DE_MAX * de

Clαβ(aero::F16Aerodynamics, α, β) = interp2d2(α, β, .2, .2, -1, 1, 8, 5, Cl_data)
Cldr(aero::F16Aerodynamics, α, β, dr) = Cl_dr(aero, α, β) * dr
Clda(aero::F16Aerodynamics, α, β, da) = Cl_da(aero, α, β) * da

Cmαde(aero::F16Aerodynamics, α, de) = interp2d(α, de, .2, 1.0/12, -1, -1, 8, 1, Cm_data)

Cnαβ(aero::F16Aerodynamics, α, β) = interp2d2(α, β, .2, .2, -1, 1, 8, 5, Cn_data)
Cndr(aero::F16Aerodynamics, α, β, dr) = Cn_dr(aero, α, β) * dr
Cnda(aero::F16Aerodynamics, α, β, da) = Cn_da(aero, α, β) * da

function calculate_aerodynamics(
    ac::Aircraft, aero::F16Aerodynamics, aerostate::AeroState, state::State
    )

   ARP = get_arp(ac)

   qinf = get_qinf(aerostate)  # Pa
   Sw = get_wing_area(ac)  # m²
   b = get_wing_span(ac)  # m
   c = get_chord(ac)  # m

   fcs = get_fcs(ac)
   de = get_value(fcs.de) * RAD2DEG
   da = get_value(fcs.da) * RAD2DEG
   dr = get_value(fcs.dr) * RAD2DEG

   α = get_alpha(aerostate) * RAD2DEG
   β = get_beta(aerostate) * RAD2DEG
   tas = get_tas(aerostate) * M2FT
   # αdot = get_alpha_dot(aerostate)  # rad/s
   p, q, r = get_body_ang_velocity(state)  # rad/s

   c2v = c * M2FT / (2*tas)
   qc2v = q * c2v
   b2v = b * M2FT / (2*tas)

   CX_q, CY_r, CY_p, CZ_q, Cl_r, Cl_p, Cm_q, Cn_r, Cn_p = dampings(aero, α)

   CX = CXαde(aero, α, de) + CX_q * qc2v
   CY = CYβ(aero, β) + CYda(aero, da) + CYdr(aero, dr) + (CY_r*r + CY_p*p) * b2v
   CZ = CZαβ(aero, α, β) + CZde(aero, de) + CZ_q * qc2v

   Cl = Clαβ(aero, α, β) +  Clda(aero, α, β, da) + Cldr(aero, α, β, dr) +
       (Cl_r*r + Cl_p*p) * b2v
   Cm = Cmαde(aero, α, de) + Cm_q*qc2v
      # + CZ * (xcgr - xcg) is taken into account in pfm
   Cn = Cnαβ(aero, α, β) + Cnda(aero, α, β, da) + Cndr(aero, α, β, dr) +
       (Cn_r*r + Cn_p*p) * b2v
      # - CY * (xcgr - xcg) * c/b is taken into account in pfm

   # Pack results
   adim_pfm_body = PointForcesMoments(ARP, [CX, CY, CZ], [Cl, Cm, Cn])

   α = get_alpha(aerostate)
   β = get_beta(aerostate)
   # Generate aerodynamics  object
   aerodynamics_from_body_coeff(adim_pfm_body, α, β, qinf, Sw, b, c, aero)
end

##----------------------------------------------------------------------------------------------------
## Propulsion

function F16Engine()
    F16Engine(
        PointForcesMoments(zeros(3), zeros(3), zeros(3)),
        0,
        0,
        0,
        # Fuel tanks can be obtained from JSBSim model
        [PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)],
        [160.0*SLUGFT2_2_KGM2, 0.0, 0.0]
        )

end

# TODO: Engine position and orientation
get_engine_position(prop::F16Engine) = [0.35 * 11.32 * FT2M, 0.0, 0.0]
get_engine_orientation(prop::F16Engine) = [0., 0., 0.] .* DEG2RAD
get_engine_gyro_effects(prop::F16Engine) = [160.0*SLUGFT2_2_KGM2, 0.0, 0.0]  # [kg·m²/s]

# ENGINE DATA
"""
    tgear(thtl)

Given the thottle setting (0 ≤ thtl ≤ 1) calculate the commanded power level
(0 ≤ cpow ≤ 100)
"""
function tgear(thtl)
    if thtl <= .77
     cpow = 64.94 * thtl
    else
     cpow = 217.38 * thtl - 117.38
    end
    return cpow
end

"""
    pdot(p3, p1)

Rate of change of power with time using a first order lag (%/s)

# Arguments
p3::Number  Actual power (%)
p1::Number  Power command (%)
"""
function pdot(p3, p1)
    if p1 >= 50.
        if p3 >= 50.
            t = 5.
            p2 = p1
        else
            p2 = 60.
            t = rtau(p2-p3)
        end
    else
        if p3 >= 50.
            t = 5.
            p2 = 40.
        else
            p2 = p1
            t = rtau(p2-p3)
        end
    end
    pd = t * (p2 - p3)
    return pd
end

"""
    rtau(dp)

Calculate the reciprocal time constant (1/s) for a first-order thrust lag given
the difference between the commanded power level (%) and the actual power
level (%).
"""
function rtau(dp)
    if dp <= 25.
        rt = 1.0  # Reciprocal time constant
    elseif dp >= 50.
        rt = 0.1
    else
        rt = 1.9 - 0.036 * dp
    end
    return rt
end

idle_data =
    [ 1060.  670.   880.   1140.  1500.  1860.
      635.   425.   690.   1010.  1330.  1700.
      60.    25.    345.   755.   1130.  1525.
     -1020. -710.  -300.   350.   910.   1360.
     -2700. -1900. -1300. -247.   600.   1100.
     -3600. -1400. -595.  -342.  -200.   700.]'

# Mil data
mil_data =
    [12680. 9150.  6200.  3950.  2450. 1400.
     12680. 9150.  6313.  4040.  2470. 1400.
     12610. 9312.  6610.  4290.  2600. 1560.
     12640. 9839.  7090.  4660.  2840. 1660.
     12390. 10176. 7750.  5320.  3250. 1930.
     11680. 9848.  8050.  6100.  3800. 2310.]'

# Max data
max_data =
    [20000. 15000. 10800. 7000.  4000. 2500.
     21420. 15700. 11225. 7323.  4435. 2600.
     22700. 16860. 12250. 8154.  5000. 2835.
     24240. 18910. 13760. 9285.  5700. 3215.
     26070. 21075. 15975. 11115. 6860. 3950.
     28886. 23319. 18300. 13484. 8642. 5057.]'

"""
    thrust(pow, alt, rmach)

Given the commanded power level (%), the altitude (ft) and the Mach number,
calculate the engine thrust (lbf)
"""
function calculate_thrust(pow, alt, rmach)

    h = 0.0001 * alt
    i = floor(Int, h)
    if i >= 5
        i=4
    end

    dh = h - float(i)

    rm = 5.0 * rmach
    m = floor(Int, rm);
    if m >= 5
        m = 4
    end

    dm = rm - float(m)
    cdh = 1.0 - float(dh)

    i=i+1;
    m=m+1;

    s = mil_data[i, m] * cdh + mil_data[i+1, m] * dh
    t = mil_data[i, m+1] *cdh + mil_data[i+1, m+1] * dh
    tmil = s + (t - s) * dm

    if pow < 50.
        s = idle_data[i, m] * cdh + idle_data[i+1, m] * dh
        t = idle_data[i,m+1] * cdh + idle_data[i+1, m+1] * dh
        tidl = s + (t - s) * dm
        thrst = tidl + (tmil - tidl) * pow * 0.02
    else
        s = max_data[i, m] * cdh + max_data[i+1, m] * dh
        t = max_data[i, m+1] * cdh + max_data[i+1, m+1] * dh
        tmax = s + (t-s) * dm
        thrst = tmil + (tmax - tmil) * (pow-50.) * 0.02
    end

    return thrst
    end

"""
The F-16 engine power response is modelled by a first-order lag (pdot function).
The rest of the model consists of thottle gearing (tgear) and lookup tables for
thrust as a function of operating power level, altitude and Mach.

Lookup tables have rows corresponding to Mach from 0 to 1 in increments of 0.2
and columns correspond to altitudes from 0 to 50000ft in increments of 10000ft.
There is a table for each power level: idle, military and maximum. Results can
be extrapolated beyond the boundaries but results may be unrealistic.

# References

Reimplemented from:

- [1] Stevens, B. L., Lewis, F. L., & Johnson, E. N. (2015). Aircraft control
 and simulation: dynamics, controls design, and autonomous systems. John Wiley
 & Sons. (page 715)
"""
function calculate_engine(
    eng::F16Engine, fcs::FCS, aerostate::AeroState, state::State; consume_fuel=false
    )

    pow = get_thrust(fcs)  # Commanded power: between 0 and 100
    Mach = get_mach(aerostate)
    # TODO: Should be altitude
    height = get_height(state) * M2FT

    thrust = calculate_thrust(pow, height, Mach)
    thrust *= LBF2N
    cj = 0.0
    Pm = thrust * get_tas(aerostate)
    ηp = NaN

    if consume_fuel==true
        error("fuel consumption not implemented")
    else
        tanks = get_tanks(eng)
    end

    # TODO: Torque calcualtion is missing
    pfm = PointForcesMoments(get_engine_position(eng),
                             [thrust, 0, 0],
                             [0, 0, 0]
                             )

    return typeof(eng)(pfm, cj, Pm, ηp, tanks, get_engine_gyro_effects(eng))
end

##----------------------------------------------------------------------------------------------------
## FCS

# TODO: Move to FCS in models
get_thrust(fcs::F16FCS) = get_value(fcs.cpl)

F16FCS(stick_lon, stick_lat, pedals, thtl, de, da, dr, cpl) = F16FCS(
    stick_lon, stick_lat, pedals, thtl, de, da, dr, cpl, false, false
    )

F16FCS() = F16FCS(
    # Cabin Inputs
    RangeControl(0.0, [0, 1]),  # stick_longitudinal
    RangeControl(0.0, [0, 1]),  # stick_lateral
    RangeControl(0.0, [0, 1]),  # pedals
    RangeControl(0.0, [0, 1]),  # thtl
    # Controls
    RangeControl(0.0, [-DE_MAX, DE_MAX] .* DEG2RAD),  # elevator
    RangeControl(0.0, [-DA_MAX, DA_MAX] .* DEG2RAD),  # ailerons
    RangeControl(0.0, [-DR_MAX, DR_MAX] .* DEG2RAD),  # rudder
    # Commanded power level
    RangeControl(0.0, [0.0, 100.]),                # CPL
    )

function set_stick_lon!(fcs::F16FCS, value)
    set_value!(fcs.stick_longitudinal, value)
    min, max = get_value_range(fcs.de)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.de, min + range * value, allow_out_of_range, throw_error)
end

function set_stick_lat!(fcs::F16FCS, value)
    set_value!(fcs.stick_lateral, value)
    min, max = get_value_range(fcs.da)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.da, min + range * value, allow_out_of_range, throw_error)
end

function set_pedals!(fcs::F16FCS, value)
    set_value!(fcs.pedals, value)
    min, max = get_value_range(fcs.dr)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.dr, min + range * value, allow_out_of_range, throw_error)
end

function set_thtl!(fcs::F16FCS, value)
    set_value!(fcs.thtl, value)
    min, max = get_value_range(fcs.thtl)
    range = max - min
    allow_out_of_range = get_allow_out_of_range_inputs(fcs)
    throw_error = get_throw_error_on_out_of_range_inputs(fcs)
    set_value!(fcs.cpl, tgear(value), allow_out_of_range, throw_error)
end

function get_controls_ranges_trimmer(fcs::F16FCS)
    [get_value_range(fcs.stick_longitudinal),
     get_value_range(fcs.stick_lateral),
     get_value_range(fcs.pedals),
     get_value_range(fcs.thtl)]
end

##----------------------------------------------------------------------------------------------------
## Aircraft Model

function F16()
    pfm0 = PointForcesMoments(zeros(3), zeros(3), zeros(3))
    aero0 = F16Aerodynamics(pfm0, pfm0, pfm0, pfm0)
    engine = F16Engine()
    propulsion0 = Propulsion(
            PointForcesMoments(zeros(3), zeros(3), zeros(3)),
            0, 0, 0,
            [engine],
            get_gyro_effects(engine)
            )

    fcs = F16FCS()
    # mass properties cannot be retrieved until ac is created... so:
    ac = F16(RigidSolid(0, zeros(3), zeros(3, 3)),
              pfm0,
              aero0,
              propulsion0,
              fcs)
    mass = get_fuel_mass_props(get_propulsion(ac)) + get_empty_mass_props(ac)
            # + get_payload_mass_props(ac)
    F16(mass, pfm0, aero0, propulsion0, fcs)
end

# Name
function get_name(ac::F16)
    return "F16"
end

# GEOMETRIC PROPERTIES
get_wing_area(ac::F16) = 300.0 * FT2M^2
get_wing_span(ac::F16) = 30. * FT2M
get_chord(ac::F16) = 11.32 * FT2M

# Aerodynamic Reference Point
# Origin assumed leading edge of CMA
get_arp(ac::F16) = [-0.35 * get_chord(ac), 0.0, 0.0]
get_empty_cg(ac::F16) = [-0.35 * get_chord(ac), 0, 0]

# MASS PROPERTIES
function get_empty_mass_props(ac::F16)
    RigidSolid(
        20500 * LB2KG,                                # Empty mass
        get_empty_cg(ac),                             # Empty CG
        [9456.        0.    -982.;                     # Empty inertia
            0.    55814.       0.;
          -982.       0.   63100.]  .* SLUGFT2_2_KGM2
    )
end

# Crew and cargo
# get_pilot_mass_props(ac::F16) = PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)
# get_cargo_mass_props(ac::F16) = PointMass(0 * LB2KG, [0., 0., 0.] .* IN2M)

# get_payload_mass_props(ac::F16) = get_pilot_mass_props(ac)

##----------------------------------------------------------------------------------------------------


@testset "Shuffle" begin

    msa_types = (
        Matrix{Residue},
        NamedResidueMatrix{Array{Residue,2}},
        MultipleSequenceAlignment,
        AnnotatedMultipleSequenceAlignment
        )

    N = length(msa_types)

    pf09645_sto = joinpath(pwd(), "data", "PF09645_full.stockholm")

    msas = [ read(pf09645_sto, Stockholm, T) for T in msa_types ]
    gaps = [ msa .== GAP for msa in msas ]
    lcol = [ mean(msa .== Residue('L'), dims=1) for msa in msas ]
    lseq = [ mean(msa .== Residue('L'), dims=2) for msa in msas ]

    Random.seed!(42)

    @testset "General" begin

        msa = msas[1]

        for dim in [1,2]

            aln = copy(msa)
            shuffle!(aln, dim)
            @test aln != msa
            @test (aln .== GAP) == gaps[1] # default: fixed gaps

            aln = copy(msa)
            shuffle!(aln, dim, true)
            @test aln != msa
            @test (aln .== GAP) == gaps[1]

            aln = copy(msa)
            shuffle!(aln, dim, false)
            @test aln != msa
            @test (aln .== GAP) != gaps[1]
        end

        @test_throws AssertionError shuffle(msa, 0, true)
        @test_throws AssertionError shuffle(msa, 3, true)
    end

    @testset "Columns" begin

        for i in 1:N
            # Fixed gaps
            msa = msas[i]
            aln = shuffle(msa, 2, true)
            @test aln != getresidues(msa)
            @test (aln .== GAP) == gaps[i]
            @test lcol[i] == mean(aln .== Residue('L'), dims=1)
            @test lseq[i] != mean(aln .== Residue('L'), dims=2)
            # Change gap positions
            aln = shuffle(msa, 2, false)
            @test aln != getresidues(msa)
            @test (aln .== GAP) != gaps[i]
            @test lcol[i] == mean(aln .== Residue('L'), dims=1)
        end
    end

    @testset "Sequences" begin

        for  i in 1:N
            # Fixed gaps
            msa = msas[i]
            aln = shuffle(msa, 1, true)
            @test aln != getresidues(msa)
            @test (aln .== GAP) == gaps[i]
            @test lcol[i] != mean(aln .== Residue('L'), dims=1)
            @test lseq[i] == mean(aln .== Residue('L'), dims=2)
            # Change gap positions
            aln = shuffle(msa, 1, false)
            @test aln != getresidues(msa)
            @test (aln .== GAP) != gaps[i]
            @test lseq[i] == mean(aln .== Residue('L'), dims=2)
        end
    end
end


using ScottishTaxBenefitModel
using .Utils
using .STBIncomes
using StaticArrays

@enum A a1=1 a2=2 a3=3 a4=4 a5=5
const ASize = Int(a5)
const MyArray = MVector{ASize,Float64}

Base.getindex( X::MyArray, s::A ) = getindex(X,Int(s))
Base.setindex!( X::MyArray, x, s::A) = setindex!(X,x,Int(s))

#
# make this illegal
#

function illegal( X::MyArray, s :: Int )
    throw( ArgumentError( "You need to index an IncomesArray with an element from the Incomes Enum"))
end

function illegal( X::MyArray, x, s :: Int )
    throw(ArgumentError( "You need to index an IncomesArray with an element from the Incomes Enum"))
end

Base.setindex!( X::MyArray, x, s::Int) = illegal( X, x, s )
Base.getindex( X::MyArray, s::Int) = illegal( X, s )

v = MyArray(zeros(5))

include( "income_enum.jl")

struct NewInc
    name :: String
    value :: Int
    label :: String
end

function writeenum( n :: NewInc )
    for i in instances( L_Incomes )
      v = Int(i)
      if v == n.value
        println( "const $(n.name) = $(n.value)")
      elseif v > n.value
        v += 1
      ned
      println( "const $i = $v")
    end
end


function Circle(arg0::Circle)
    return Circle((Circle,), arg0)
end

function Circle(arg0::S2Point, arg1::S2Point, arg2::jdouble)
    return Circle((S2Point, S2Point, jdouble), arg0, arg1, arg2)
end

function Circle(arg0::Vector3D, arg1::jdouble)
    return Circle((Vector3D, jdouble), arg0, arg1)
end

function copy_self(obj::Circle)
    return jcall(obj, "copySelf", Circle, ())
end

function empty_hyperplane(obj::Circle)
    return jcall(obj, "emptyHyperplane", SubCircle, ())
end

function equals(obj::Object, arg0::Object)
    return jcall(obj, "equals", jboolean, (Object,), arg0)
end

function get_arc(obj::Circle, arg0::S2Point, arg1::S2Point)
    return jcall(obj, "getArc", Arc, (S2Point, S2Point), arg0, arg1)
end

function get_class(obj::Object)
    return jcall(obj, "getClass", Class, ())
end

function get_inside_arc(obj::Circle, arg0::Circle)
    return jcall(obj, "getInsideArc", Arc, (Circle,), arg0)
end

function get_offset(obj::Circle, arg0::Point)
    return jcall(obj, "getOffset", jdouble, (Point,), arg0)
end

function get_offset(obj::Circle, arg0::Vector3D)
    return jcall(obj, "getOffset", jdouble, (Vector3D,), arg0)
end

function get_phase(obj::Circle, arg0::Vector3D)
    return jcall(obj, "getPhase", jdouble, (Vector3D,), arg0)
end

function get_point_at(obj::Circle, arg0::jdouble)
    return jcall(obj, "getPointAt", Vector3D, (jdouble,), arg0)
end

function get_pole(obj::Circle)
    return jcall(obj, "getPole", Vector3D, ())
end

function get_reverse(obj::Circle)
    return jcall(obj, "getReverse", Circle, ())
end

function get_tolerance(obj::Circle)
    return jcall(obj, "getTolerance", jdouble, ())
end

function get_transform(::Type{Circle}, arg0::Rotation)
    return jcall(Circle, "getTransform", Transform, (Rotation,), arg0)
end

function get_x_axis(obj::Circle)
    return jcall(obj, "getXAxis", Vector3D, ())
end

function get_y_axis(obj::Circle)
    return jcall(obj, "getYAxis", Vector3D, ())
end

function hash_code(obj::Object)
    return jcall(obj, "hashCode", jint, ())
end

function notify(obj::Object)
    return jcall(obj, "notify", void, ())
end

function notify_all(obj::Object)
    return jcall(obj, "notifyAll", void, ())
end

function project(obj::Circle, arg0::Point)
    return jcall(obj, "project", Point, (Point,), arg0)
end

function reset(obj::Circle, arg0::Vector3D)
    return jcall(obj, "reset", void, (Vector3D,), arg0)
end

function revert_self(obj::Circle)
    return jcall(obj, "revertSelf", void, ())
end

function same_orientation_as(obj::Circle, arg0::Hyperplane)
    return jcall(obj, "sameOrientationAs", jboolean, (Hyperplane,), arg0)
end

function to_space(obj::Circle, arg0::Point)
    return jcall(obj, "toSpace", S2Point, (Point,), arg0)
end

function to_string(obj::Object)
    return jcall(obj, "toString", JString, ())
end

function to_sub_space(obj::Circle, arg0::Point)
    return jcall(obj, "toSubSpace", S1Point, (Point,), arg0)
end

function wait(obj::Object)
    return jcall(obj, "wait", void, ())
end

function wait(obj::Object, arg0::jlong)
    return jcall(obj, "wait", void, (jlong,), arg0)
end

function wait(obj::Object, arg0::jlong, arg1::jint)
    return jcall(obj, "wait", void, (jlong, jint), arg0, arg1)
end

function whole_hyperplane(obj::Circle)
    return jcall(obj, "wholeHyperplane", SubCircle, ())
end

function whole_space(obj::Circle)
    return jcall(obj, "wholeSpace", SphericalPolygonsSet, ())
end


"""
GenX: An Configurable Capacity Expansion Model
Copyright (C) 2021,  Massachusetts Institute of Technology
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
A complete copy of the GNU General Public License v2 (GPLv2) is available
in LICENSE.txt.  Users uncompressing this from an archive may not have
received this license file.  If not, see <http://www.gnu.org/licenses/>.
"""

@doc raw"""
	fix_integers(jump_model::Model)

inputs: jump_model - a model object containing that has been previously solved.

description: fixes the iteger variables ones the model has been solved in order to calculate approximations of dual variables

returns: none (modifies an existing-solved model in the memory). solve() must be run again to solve and getdual veriables

"""
function fix_integers(jump_model::Model)
	################################################################################
	## function fix_integers()
	##
	## inputs: jump_model - a model object containing that has been previously solved.
	##
	## description: fixes the iteger variables ones the model has been solved in order
	## to calculate approximations of dual variables
	##
	## returns: no result since it modifies an existing-solved model in the memory.
	## solve() must be run again to solve and getdual veriables
	##
	################################################################################
	values = Dict(v => value(v) for v in all_variables(jump_model))
	for v in all_variables(jump_model)
		if is_integer(v)
            fix(v,values[v],force=true)
			unset_integer(v)
        elseif is_binary(v)
            fix(v,values[v],force=true)
			unset_binary(v)
        end
	end
end

@doc raw"""
	function solve_model()

inputs: EP - a JuMP model representing the energy optimization problem
setup - a Dict containing GenX setup flags

description: Solves and extracts solution variables for later processing

returns: results EP model object with a set of DataFrames containing key results
"""
function solve_model(EP::Model, setup::Dict)
	################################################################################
	## function solve_model()
	##
	## inputs: EP - a JuMP model representing the energy optimization problem
	## setup - a Dict containing GenX setup flags
	##
	## description: Solves and extracts solution variables for later processing
	##
	## returns: results EP model object with a set of DataFrames containing key results
	##
	################################################################################

	println("Solving Model")

	## Start solve timer
	solver_start_time = time()
	solver_time = time()

	## Solve Model
	optimize!(EP)

	if has_duals(EP) # fully linear model
		println("LP solved for primal")
	else
		println("MILP solved for primal")
	end

	if !has_duals(EP) && setup["WriteShadowPrices"] == 1
		# function to fix integers and linearize problem
		fix_integers(EP)
		# re-solve statement for LP solution
		println("Solving LP solution for duals")
		optimize!(EP)
	end

	## Record solver time
	solver_time = time() - solver_start_time

	return EP, solver_time
end


@testset "plotting" begin

    x = rand(10)
    f = GP(SqExponentialKernel())
    gp = f(x)
    plt1 = sampleplot(gp, 10)
    @test plt1.n == 10

    rec1 = RecipesBase.apply_recipe(Dict{Symbol, Any}(), gp)
    @test rec1[1].args[1] ≈ x atol=1e-5
    @test rec1[1].args[2] ≈ zero(x) atol=1e-5

    rec2 = RecipesBase.apply_recipe(Dict{Symbol, Any}(), f, rand(10))
    @test typeof(rec2[1].args[1]) <: AbstractGPs.FiniteGP

    rec3 = RecipesBase.apply_recipe(Dict{Symbol, Any}(), f, 0:0.01:1)
    @test typeof(rec3[1].args[1]) <: AbstractGPs.FiniteGP

    rec4 = RecipesBase.apply_recipe(Dict{Symbol, Any}(), f, 0, 1)
    @test typeof(rec4[1].args[1]) <: AbstractGPs.FiniteGP
end


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
    NoIntegerAlgorithm

Empty functor object for running an L-shaped algorithm without dealing with integer variables.

"""
struct NoIntegerAlgorithm <: AbstractIntegerAlgorithm
    skip_check::Bool
end

function initialize_integer_algorithm!(integer::NoIntegerAlgorithm, first_stage::JuMP.Model)
    # Sanity check
    if !integer.skip_check &&
       any(is_binary, all_decision_variables(first_stage, 1)) ||
       any(is_integer, all_decision_variables(first_stage, 1))
        @warn "First-stage has binary/integer decisions and no `IntegerStrategy` has been set. Procedure will fail if second-stage has binary/integer variables. Otherwise, the master_optimizer must be integer-capable."
    end
    return nothing
end

function initialize_integer_algorithm!(integer::NoIntegerAlgorithm, subproblem::SubProblem)
    if !integer.skip_check &&
       any(is_binary, all_decision_variables(subproblem.model, StochasticPrograms.stage(subproblem.model))) ||
       any(is_integer, all_decision_variables(subproblem.model, StochasticPrograms.stage(subproblem.model))) ||
       any(is_binary, all_auxiliary_variables(subproblem.model)) ||
       any(is_integer, all_auxiliary_variables(subproblem.model))
        error("Second-stage has binary/integer decisions. Rerun procedure with an `IntegerStrategy`.")
    end
    return nothing
end

function handle_integrality!(lshaped::AbstractLShaped, ::NoIntegerAlgorithm)
    # Ensure any binary/integer decisions are rounded
    for (i,dvar) in enumerate(all_decision_variables(lshaped.structure.first_stage, 1))
        if is_binary(dvar) || is_integer(dvar)
            lshaped.x[i] = round(lshaped.x[i])
        end
    end
    return nothing
end

function integer_variables(::NoIntegerAlgorithm)
    return MOI.VariableIndex[]
end

function check_optimality(::AbstractLShaped, ::NoIntegerAlgorithm)
    return true
end

function solve_subproblem(subproblem::SubProblem,
                          metadata,
                          ::NoFeasibilityAlgorithm,
                          ::NoIntegerAlgorithm,
                          x::AbstractVector)
    return solve_subproblem(subproblem, x)
end

function solve_subproblem(subproblem::SubProblem,
                          metadata,
                          feasibility_algorithm::FeasibilityCutsWorker,
                          ::NoIntegerAlgorithm,
                          x::AbstractVector)
    model = subproblem.optimizer
    if !prepared(feasibility_algorithm)
        prepare!(model, feasibility_algorithm)
    else
        activate!(model, feasibility_algorithm)
    end
    # Optimize auxiliary problem
    MOI.optimize!(model)
    # Sanity check that aux problem could be solved
    status = MOI.get(subproblem.optimizer, MOI.TerminationStatus())
    if !(status ∈ AcceptableTermination)
        error("Subproblem $(subproblem.id) was not solved properly during feasibility check, returned status code: $status")
    end
    sense = MOI.get(subproblem.optimizer, MOI.ObjectiveSense())
    correction = (sense == MOI.MIN_SENSE || sense == MOI.FEASIBILITY_SENSE) ? 1.0 : -1.0
    w = correction * MOI.get(model, MOI.ObjectiveValue())
    # Ensure correction is available in master
    set_metadata!(metadata, subproblem.id, :correction, correction)
    # Check feasibility
    if w > sqrt(eps())
        # Subproblem is infeasible, create feasibility cut
        return FeasibilityCut(subproblem, x)
    end
    # Restore subproblem and solve as usual
    deactivate!(model, feasibility_algorithm)
    return solve_subproblem(subproblem, x)
end

"""
    IgnoreIntegers

Factory object for [`NoIntegerAlgorithm`](@ref). Passed by default to `integer_strategy` in `LShaped.Optimizer`.

"""
struct IgnoreIntegers <: AbstractIntegerStrategy
    skip_check::Bool

    IgnoreIntegers(skip_check::Bool) = new(skip_check)
end

IgnoreIntegers(; skip_check = false) = IgnoreIntegers(skip_check)

function master(ignore::IgnoreIntegers, ::Type{T}) where T <: AbstractFloat
    return NoIntegerAlgorithm(ignore.skip_check)
end

function worker(ignore::IgnoreIntegers, ::Type{T}) where T <: AbstractFloat
    return NoIntegerAlgorithm(ignore.skip_check)
end
function worker_type(::IgnoreIntegers)
    return NoIntegerAlgorithm
end


using Oceananigans: instantiated_location
using Oceananigans.Architectures: arch_array, device_event, device_copy_to!
using Oceananigans.Operators: assumed_field_location
using Oceananigans.Fields: reduced_dimensions

using Oceananigans.BoundaryConditions:
            ContinuousBoundaryFunction,
            DiscreteBoundaryFunction,
            permute_boundary_conditions,
            fill_halo_event!,
            CBCT,
            CBC

import Oceananigans.Fields: tupled_fill_halo_regions!, boundary_conditions, data

import Oceananigans.BoundaryConditions:
            fill_halo_regions!,
            fill_west_and_east_halo!,
            fill_south_and_north_halo!,
            fill_bottom_and_top_halo!,
            fill_west_halo!,
            fill_east_halo!,
            fill_south_halo!,
            fill_north_halo!

@inline bc_str(::MultiRegionObject) = "MultiRegion Boundary Conditions"

@inline extract_field_buffers(field::Field)          = field.boundary_buffers
@inline boundary_conditions(field::MultiRegionField) = field.boundary_conditions

# This can be implemented once we have a buffer for field_tuples
@inline function tupled_fill_halo_regions!(full_fields, grid::MultiRegionGrid, args...; kwargs...)
    for field in full_fields
        fill_halo_regions!(field, args...; kwargs...)
    end
end

function fill_halo_regions!(field::MultiRegionField, args...; kwargs...)
    reduced_dims = reduced_dimensions(field)

    return fill_halo_regions!(field.data,
                              field.boundary_conditions,
                              instantiated_location(field),
                              field.grid,
                              field.boundary_buffers,
                              args...;
                              reduced_dimensions = reduced_dims,
                              kwargs...)
end

fill_halo_regions!(c::MultiRegionObject, ::Nothing, args...; kwargs...) = nothing

#####
##### fill_halo_regions! for a MultiRegionObject
#####

# this can be used once we don't need to fill Value, Flux and Gradient anymore!
# fill_halo_regions!(c::MultiRegionObject, bcs, loc, mrg::MultiRegionGrid, buffers, args...; kwargs...) =
#     apply_regionally!(fill_halo_regions!, c, bcs, loc, mrg, Reference(c.regions), Reference(buffers.regions), args...; kwargs...)

function fill_halo_regions!(c::MultiRegionObject, bcs, loc, mrg::MultiRegionGrid, buffers, args...; kwargs...)

    arch = architecture(mrg)

    halo_tuple = construct_regionally(permute_boundary_conditions, bcs)

    for task = 1:3
        barrier = device_event(arch)
        apply_regionally!(fill_halo_event!, task, halo_tuple,
                          c, loc, arch, barrier, mrg, Reference(c.regions), Reference(buffers.regions),
                          args...; kwargs...)
    end

    return nothing
end

#####
##### fill_halo! for Communicating boundary condition
#####

## Fill communicating boundary condition halos
for (lside, rside) in zip([:west, :south, :bottom], [:east, :north, :bottom])
    fill_both_halo! = Symbol(:fill_, lside, :_and_, rside, :_halo!)
    fill_left_halo!  = Symbol(:fill_, lside, :_halo!)
    fill_right_halo! = Symbol(:fill_, rside, :_halo!)

    @eval begin
        function $fill_both_halo!(c, left_bc::CBC, right_bc, loc, arch, dep, grid, args...; kwargs...)
            event = $fill_right_halo!(c, right_bc, arch, dep, grid, args...; kwargs...)
            $fill_left_halo!(c,  left_bc, arch, event, grid, args...; kwargs...)
            return NoneEvent()
        end
        function $fill_both_halo!(c, left_bc, right_bc::CBC, loc, arch, dep, grid, args...; kwargs...)
            event = $fill_left_halo!(c, left_bc, arch, dep, grid, args...; kwargs...)
            $fill_right_halo!(c, right_bc, arch, event, grid, args...; kwargs...)
            return NoneEvent()
        end
    end
end

function fill_west_and_east_halo!(c, westbc::CBC, eastbc::CBC, loc, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[1]
    N = size(grid)[1]
    w = neighbors[westbc.condition.from_rank]
    e = neighbors[eastbc.condition.from_rank]

    westdst = buffers[westbc.condition.rank].west.recv
    eastdst = buffers[eastbc.condition.rank].east.recv

    wait(device(arch), dep)

    switch_device!(getdevice(w))
    westsrc = buffers[westbc.condition.from_rank].east.send
    westsrc .= view(parent(w), N+1:N+H, :, :)

    switch_device!(getdevice(e))
    eastsrc = buffers[eastbc.condition.from_rank].west.send
    eastsrc .= view(parent(e), H+1:2H, :, :)

    switch_device!(getdevice(c))
    device_copy_to!(westdst, westsrc)
    device_copy_to!(eastdst, eastsrc)

    view(parent(c), 1:H, :, :)        .= westdst
    view(parent(c), N+H+1:N+2H, :, :) .= eastdst

    return NoneEvent()
end

function fill_south_and_north_halo!(c, southbc::CBC, northbc::CBC, loc, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[2]
    N = size(grid)[2]
    s = neighbors[southbc.condition.from_rank]
    n = neighbors[northbc.condition.from_rank]

    southdst = buffers[southbc.condition.rank].south.recv
    northdst = buffers[northbc.condition.rank].north.recv

    wait(device(arch), dep)

    switch_device!(getdevice(s))
    southsrc = buffers[westbc.condition.from_rank].south.send
    southsrc .= view(parent(s), :, N+1:N+H, :)

    switch_device!(getdevice(n))
    northsrc = buffers[eastbc.condition.from_rank].north.send
    northsrc .= view(parent(n), :, H+1:2H, :)

    switch_device!(getdevice(c))
    device_copy_to!(southdst, southsrc)
    device_copy_to!(northdst, northsrc)

    view(parent(c), :, 1:H, :, :)        .= southdst
    view(parent(c), :, N+H+1:N+2H, :, :) .= northdst

    return NoneEvent()
end

#####
##### Single fill_halo! for Communicating boundary condition
#####

function fill_west_halo!(c, bc::CBC, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[1]
    N = size(grid)[1]
    w = neighbors[bc.condition.from_rank]
    dst = buffers[bc.condition.rank].west.recv

    wait(device(arch), dep)

    switch_device!(getdevice(w))
    src = buffers[bc.condition.from_rank].east.send
    src .= view(parent(w), N+1:N+H, :, :)
    sync_device!(getdevice(w))

    switch_device!(getdevice(c))
    device_copy_to!(dst, src)

    p  = view(parent(c), 1:H, :, :)
    p .= dst

    return nothing
end

function fill_east_halo!(c, bc::CBC, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[1]
    N = size(grid)[1]
    e = neighbors[bc.condition.from_rank]
    dst = buffers[bc.condition.rank].east.recv

    wait(device(arch), dep)

    switch_device!(getdevice(e))
    src = buffers[bc.condition.from_rank].west.send
    src .= view(parent(e), H+1:2H, :, :)
    sync_device!(getdevice(e))

    switch_device!(getdevice(c))
    device_copy_to!(dst, src)

    p  = view(parent(c), N+H+1:N+2H, :, :)
    p .= dst

    return nothing
end

function fill_south_halo!(c, bc::CBC, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[2]
    N = size(grid)[2]
    s = neighbors[bc.condition.from_rank]
    dst = buffers[bc.condition.rank].south.recv

    wait(device(arch), dep)

    switch_device!(getdevice(s))
    src = buffers[bc.condition.from_rank].north.send
    src .= view(parent(s), :, N+1:N+H, :)
    sync_device!(getdevice(s))

    switch_device!(getdevice(c))
    device_copy_to!(dst, src)

    p  = view(parent(c), :, 1:H, :)
    p .= dst

    return nothing
end

function fill_north_halo!(c, bc::CBC, arch, dep, grid, neighbors, buffers, args...; kwargs...)

    H = halo_size(grid)[2]
    N = size(grid)[2]
    n = neighbors[bc.condition.from_rank]
    dst = buffers[bc.condition.rank].north.recv

    wait(device(arch), dep)

    switch_device!(getdevice(n))
    src = buffers[bc.condition.from_rank].south.send
    src .= view(parent(n), :, H+1:2H, :)
    sync_device!(getdevice(n))

    switch_device!(getdevice(c))
    device_copy_to!(dst, src)

    p  = view(parent(c), :, N+H+1:N+2H, :)
    p .= dst

    return nothing
end

#####
##### MultiRegion boundary condition utils
#####

@inline getregion(fc::FieldBoundaryConditions, i) =
        FieldBoundaryConditions(_getregion(fc.west, i),
                                _getregion(fc.east, i),
                                _getregion(fc.south, i),
                                _getregion(fc.north, i),
                                _getregion(fc.bottom, i),
                                _getregion(fc.top, i),
                                fc.immersed)

@inline getregion(bc::BoundaryCondition, i) = BoundaryCondition(bc.classification, _getregion(bc.condition, i))

@inline getregion(cf::ContinuousBoundaryFunction{X, Y, Z, I}, i) where {X, Y, Z, I} =
    ContinuousBoundaryFunction{X, Y, Z, I}(cf.func::F,
                                           _getregion(cf.parameters, i),
                                           cf.field_dependencies,
                                           cf.field_dependencies_indices,
                                           cf.field_dependencies_interp)

@inline getregion(df::DiscreteBoundaryFunction, i) =
    DiscreteBoundaryFunction(df.func, _getregion(df.parameters, i))

@inline _getregion(fc::FieldBoundaryConditions, i) =
FieldBoundaryConditions(getregion(fc.west, i),
                        getregion(fc.east, i),
                        getregion(fc.south, i),
                        getregion(fc.north, i),
                        getregion(fc.bottom, i),
                        getregion(fc.top, i),
                        fc.immersed)

@inline _getregion(bc::BoundaryCondition, i) = BoundaryCondition(bc.classification, getregion(bc.condition, i))

@inline _getregion(cf::ContinuousBoundaryFunction{X, Y, Z, I}, i) where {X, Y, Z, I} =
ContinuousBoundaryFunction{X, Y, Z, I}(cf.func::F,
                                   getregion(cf.parameters, i),
                                   cf.field_dependencies,
                                   cf.field_dependencies_indices,
                                   cf.field_dependencies_interp)

@inline _getregion(df::DiscreteBoundaryFunction, i) =
DiscreteBoundaryFunction(df.func, getregion(df.parameters, i))

# Everything goes for multi-region BC
validate_boundary_condition_location(::MultiRegionObject, ::Center, side)       = nothing
validate_boundary_condition_location(::MultiRegionObject, ::Face, side)         = nothing
validate_boundary_condition_topology(::MultiRegionObject, topo::Periodic, side) = nothing
validate_boundary_condition_topology(::MultiRegionObject, topo::Flat,     side) = nothing


#
# The roll-up combinator.
#

function RollUpQuery(qbase::Query, qks::Vector{Query})
    for qk in qks
        @assert fits(output(qbase), input(qk))
        @assert isplain(qk)
    end
    q = RecordQuery(
            qbase >>
            RecordQuery(ostub(qbase), qks))
    q = q >>
        Query(
            RollUpSig(),
            Input(domain(output(q))),
            Output([
                Output([
                    domain(output(qbase)),
                    (output(qk) |> setoptional() |> decorate(:nullrev => true) for qk in qks)...]) |> setoptional(isoptional(qbase)) |> setplural()]))
    q >>
    Query(
        GroupSig(),
        Input(domain(output(q))),
        Output([output(qbase) |> setoptional(false), (output(qk) |> setoptional(true) for qk in qks)...])
            |> setoptional(isoptional(qbase))
            |> setplural(isplural(qbase) && !isempty(qks)))

end

RollUpQuery(qbase::Query, qks::Query...) =
    RollUpQuery(qbase, collect(Query, qks))

immutable RollUpSig <: AbstractPrimitive
end

ev(sig::RollUpSig, ::Input, oty::Output, iflow::InputFlow) =
    OutputFlow(oty, ev(sig, values(iflow), fields(domain(iflow))...))

ev(::RollUpSig, dv::DataVector, oty::Output) =
    rollup_impl(column(dv, 1), fields(oty))

function rollup_impl{OPT,PLU}(dvcol::Column{OPT,PLU}, fs::Vector{Output})
    offs = offsets(dvcol)
    dv = values(dvcol)
    width = length(dv.cols)
    groupoffs = rollup_offsets_impl(width, offs)
    fs = Vector{Column}(width)
    fs[1] = rollup_values_impl(1, width, offs, values(dv, 1))
    for k = 2:width
        fs[k] = rollup_values_impl(k, width, offs, values(dv, k))
    end
    return PlainColumn(
                DataVector(
                    length(dvcol),
                    Column{OPT,true}(
                        groupoffs,
                        DataVector(width*length(dv), fs))))
end

function rollup_offsets_impl(width::Int, offs::AbstractVector{Int})
    offs′ = Vector{Int}(length(offs))
    for k = 1:endof(offs)
        offs′[k] = width * (offs[k] - 1) + 1
    end
    return offs′
end

function rollup_values_impl(k::Int, width::Int, offs::AbstractVector{Int}, vals::AbstractVector)
    len = length(vals)
    offs′ = Vector{Int}(len * width + 1)
    offs′[1] = 1
    n = 1
    m = 1
    idxs = Vector{Int}(len * (width-k+1))
    for i = 1:endof(offs)-1
        l = offs[i]
        r = offs[i+1]
        for p = 1:(width-k+1)
            for j = l:r-1
                idxs[n] = j
                n += 1
                offs′[m+1] = n
                m += 1
            end
        end
        for p = (width-k+2):width
            for j = l:r-1
                offs′[m+1] = n
                m += 1
            end
        end
    end
    return Column{k!=1,false}(offs′, vals[idxs])
end


type LabelerFF
    word_f
    tag_f
    label_f
    nonlinear
    W
end

const none = 46

function setheads!{T}(s::State{T})
    @assert isfinal(s)
    s.head = nullstate(T)
    stack = [s]
    while !isempty(stack)
        s = pop!(stack)
        if !isnull(s.lchild)
            s.lchild.head = s
            push!(stack, s.lchild)
        end
        st = s
        while !isnull(st.lsibl.lchild)
            st.lsibl.lchild.head = s
            push!(stack, st.lsibl.lchild)
            st = st.lsibl
        end
        if !isnull(s.rchild)
            s.rchild.head = s
            push!(stack, s.rchild)
        end
        st = s
        while !isnull(st.rsibl.rchild)
            st.rsibl.rchild.head = s
            push!(stack, st.rsibl.rchild)
            st = st.rsibl
        end
    end
end

type LExample
    w::Vector{Int}
    t::Union{Matrix{Float32},Vector{Int}}
    l::Vector{Int}
    target::Int
end

function gen_trainexample(s::State)
    sent = s.tokens
    labelsize = targetsize(s.parser.labeler)
    w, t = genfeatures(s)
    l = Int[] # [label(s0 -> s0head), label(s0head -> s0head's head)]
    push!(l, sent[s.top].label)
    push!(l, tokenat(s, s.head).label)
    push!(l, tokenat(s, s.head.rchild).label)
    push!(l, tokenat(s, s.head.lchild).label)
    push!(l, tokenat(s, s.head.head.rchild).label)
    push!(l, tokenat(s, s.head.head.lchild).label)
    target = sent[s.top].label
    LExample(w, t, l, target)
end

targetsize(m::LabelerFF) = size(m.W[end].w.data)[1]

function initmodel!(parser::DepParser, labeler::Type{LabelerFF}, embed,
    topktags, nonlinear, sparsesizes, embedsizes, hiddensizes; inherit=false)
    T = Float32
    if inherit
        word_f = Embedding(copy(parser.model.word_f.ws), IntSet())
        tag_f  = Embedding(copy(parser.model.tag_f.ws), IntSet())
        label_f = Embedding(copy(parser.model.label_f.ws), IntSet())
    else
        if embed == ""
            info("USING EMBEDDINGS WITH UNIFORM DISTRIBUTION [-0.01, 0.01]")
            word_f = Embedding(T, length(parser.words), embedsizes[1])
        else
            info("USING EMBEDDINGS LOADED FROM $(embed)")
            word_f = Embedding(embed, Float32)
            scale!(word_f)
        end
        if topktags
            tag_f = Linear(T, length(parser.tags), embedsizes[2])
        else
            tag_f = Embedding(T, length(parser.tags), embedsizes[2])
        end
        label_f = Embedding(T, length(parser.labels), embedsizes[2])
    end
    indim = sum(sparsesizes .* embedsizes)
    outdim = length(parser.labels) - 1 # exclude NONE
    W = [myLinear(T, indim, hiddensizes[1]),
         myLinear(T, hiddensizes[1], outdim)]
    parser.labeler = LabelerFF(word_f, tag_f, label_f, nonlinear, W)
    info("INPUT: [S^word,S^tag,S^label] = ", sparsesizes)
    info("EMBED DIMS: [word,tag,label] = ", embedsizes)
    info("HIDDEN LAYER: ", hiddensizes)
    info("OUTPUT DIM: ", outdim)
    info("NONLINEAR: ", nonlinear)
end

# TODO: make State have id field
# to tell where the State is in a batch
# called from expand(::State ::Int)
@compat function (m::LabelerFF){T}(s::State{T}, act::Int)
    Var([0f0])
end

@compat function (m::LabelerFF)(batch::AbstractVector, istrain=true)
    wordvec, tagvec, labelvec = [], [], []
    for s in batch
        push!(wordvec, s[1])
        push!(tagvec, s[2])
        push!(labelvec, s[3])
    end
    wordmat = m.word_f(Var(hcat(wordvec...)))
    tagmat = m.tag_f(Var(hcat(tagvec...)))
    labelmat = m.label_f(Var(hcat(labelvec...)))
    x = concat(1, wordmat, tagmat, labelmat)
    x = m.nonlinear(m.W[1](x))
    x = dropout(x, 0.5, istrain)
    x = m.W[end](x)
    x
end

function genfeatures(s::State)
    # word, tag
    s0     = tokenat(s, s.top)
    s0l    = tokenat(s, s.lchild)
    s0ll1  = tokenat(s, s.lchild.lchild)
    s0ll2  = tokenat(s, s.lchild.lsibl.lchild)
    s0lr1  = tokenat(s, s.lchild.rchild)
    s0lr2  = tokenat(s, s.lchild.rsibl.rchild)
    s0l2   = tokenat(s, s.lsibl.lchild)
    s0l2l1 = tokenat(s, s.lsibl.lchild.lchild)
    s0l2l2 = tokenat(s, s.lsibl.lchild.lsibl.lchild)
    s0l2r1 = tokenat(s, s.lsibl.lchild.rchild)
    s0l2r2 = tokenat(s, s.lsibl.lchild.rsibl.rchild)
    s0l3   = tokenat(s, s.lsibl.lsibl.lchild)
    s0r    = tokenat(s, s.rchild)
    s0rr1  = tokenat(s, s.rchild.rchild)
    s0rr2  = tokenat(s, s.rchild.rsibl.rchild)
    s0rl1  = tokenat(s, s.rchild.lchild)
    s0rl2  = tokenat(s, s.rchild.lsibl.lchild)
    s0r2   = tokenat(s, s.rsibl.rchild)
    s0r2r1 = tokenat(s, s.rsibl.rchild.rchild)
    s0r2r2 = tokenat(s, s.rsibl.rchild.rsibl.rchild)
    s0r2l1 = tokenat(s, s.rsibl.rchild.lchild)
    s0r2l2 = tokenat(s, s.rsibl.rchild.lsibl.lchild)
    s0r3   = tokenat(s, s.rsibl.rsibl.rchild)

    s0h   = tokenat(s, s.head)
    s0h2  = tokenat(s, s.head.head)
    s0hl  = tokenat(s, s.head.lchild)
    s0hr  = tokenat(s, s.head.rchild)
    s0h2l  = tokenat(s, s.head.head.lchild)
    s0h2r  = tokenat(s, s.head.head.rchild)

    words = [s0.word, s0l.word, s0ll1.word, s0ll2.word, s0lr1.word, s0lr2.word,
             s0l2.word, s0l2l1.word, s0l2l2.word, s0l2r1.word, s0l2r2.word, s0l3.word,
             s0.word, s0r.word, s0rr1.word, s0rr2.word, s0rl1.word, s0rl2.word,
             s0r2.word, s0r2r1.word, s0r2r2.word, s0r2l1.word, s0r2l2.word, s0r3.word,
             s0h.word, s0h2.word, s0hl.word, s0hr.word, s0h2l.word, s0h2r.word]

    tags = [s0.tag, s0l.tag, s0ll1.tag, s0ll2.tag, s0lr1.tag, s0lr2.tag,
             s0l2.tag, s0l2l1.tag, s0l2l2.tag, s0l2r1.tag, s0l2r2.tag, s0l3.tag,
             s0.tag, s0r.tag, s0rr1.tag, s0rr2.tag, s0rl1.tag, s0rl2.tag,
             s0r2.tag, s0r2r1.tag, s0r2r2.tag, s0r2l1.tag, s0r2l2.tag, s0r3.tag,
             s0h.tag, s0h2.tag, s0hl.tag, s0hr.tag, s0h2l.tag, s0h2r.tag]

    # words = [s0.word, s0l.word, s0l2.word, s0l3.word, s02l.word, s02l2.word, s0r.word,
    #  s0r2.word, s0r3.word, s02r.word, s02r2.word, s0h.word, s0h2.word]
    # tags = [s0.tag, s0l.tag, s0l2.tag, s0l3.tag, s02l.tag, s02l2.tag, s0r.tag,
    #         s0r2.tag, s0r3.tag, s02r.tag, s02r2.tag, s0h.tag, s0h2.tag,]
    return words, tags
end

# function getchildren(heads::Array{Int})
#     res = map(_ -> [], heads)
#     for (c, h) in enumerate(heads)
#         push!(res[h], c)
#     end
#     res
# end

# function head2state{T}(s::State{T})
#     @assert isfinal(s) && s.top == 0
#     res = Array(State{T}, length(s.tokens))
#     while !isnull(s.prev)
#         s = s.prev
#         if s.top != 0 && !isdefined(res, s.top)
#             res[s.top] = s
#         end
#     end
#     res
# end

function assign!{T}(labeler::LabelerFF, ss::Vector{State{T}})
    # predicted labels
    blabels = map(s ->fill(none, length(s.tokens)), ss)
    bheads  = map(s -> heads(s), ss)
    stack = map(i -> (ss[i].prev ,i), 1:length(ss)) # (batches i'th tree, batchid)
    while !isempty(stack)
        bsize = min(length(stack), 10000)
        batch, stack = stack[1:bsize], stack[bsize+1:end]
        bfeat = map(batch) do tup
            s, bid = tup
            w, t = genfeatures(s)
    push!(l, tokenat(s, s.head.rchild).label)
    push!(l, tokenat(s, s.head.lchild).label)
    push!(l, tokenat(s, s.head.head.rchild).label)
    push!(l, tokenat(s, s.head.head.lchild).label)
            l = [blabels[bid][s.top],
                get(blabels[bid], bheads[bid][s.top], none),
                s.head.rchild.top != 0 ? blabels[bid][bheads[bid][s.head.rchild.top]] : none,
                s.head.lchild.top != 0 ? blabels[bid][bheads[bid][s.head.lchild.top]] : none,
                s.head.head.rchild.top != 0 ? blabels[bid][bheads[bid][s.head.head.rchild.top]] : none,
                s.head.head.lchild.top != 0 ? blabels[bid][bheads[bid][s.head.head.lchild.top]] : none]
            (w, t, l)
        end
        preds = argmax(labeler(bfeat, false).data, 1)
        for (i, (s, bid)) in enumerate(batch)
            blabels[bid][s.top] = preds[i]
            !isnull(s.lchild) && push!(stack, (s.lchild, bid))
            st = s
            while !isnull(st.lsibl.lchild)
                push!(stack, (st.lsibl.lchild, bid))
                st = st.lsibl
            end
            !isnull(s.rchild) && push!(stack, (s.rchild, bid))
            st = s
            while !isnull(st.rsibl.rchild)
                push!(stack, (st.rsibl.rchild, bid))
                st = st.rsibl
            end
        end
    end
    @assert all(la -> all(v -> v != none, la), blabels)
    blabels
end
# if  && blabels[bid][s.(arc).top] == none

# function assign!{T}(labeler::LabelerFF, ss::Vector{State{T}})
#     @assert all(isfinal, ss)
#     # predicted labels
#     blabels   = map(s -> fill(none, length(s.tokens)), ss)
#     bstates   = map(s -> head2state(s), ss) # no State with s.top == 0
#     bheads    = map(s -> heads(s), ss)
#     bchildren = map(heads -> getchildren(heads), bheads)
#     stack = map(i -> (1,i), 1:length(ss)) # (headid, batchid)
#     while !isempty(stack)
#         bsize = min(length(stack), length(ss))
#         batch, stack = stack[1:bsize], stack[bsize:end]
#         bfeat = map(batch) do tup
#             hid, bid = tup
#             s = bstates[bid][hid]
#             w, t = genfeatures(s)
#             l = [labels[bid][s.top],
#                  heads[bid][s.top] != 0 ? labels[bid][heads[bid][s.top]] : 1]
#             (w, t, l)
#         end
#         preds = argmax(parser.labeler(bfeat, false).data, 1)
#         for (i, (hid, bid)) in enumarate(batch)
#             blabels[bid][hid] = preds[i]
#             for childid in bchildren
#                 push!(stack, (childid, bid))
#             end
#         end
#     end
#     blabels
# end
#
typealias Doc Vector{Vector{Token}}
function train!{T}(::Type{LabelerFF}, parser::DepParser{T}, trainsents::Doc,
    testsents::Doc=Vector{Token}[]; embed="", batchsize=32, iter=20, progbar=true,
    nonlinear=tanh, sparsesizes=[30,30,6] ,embedsizes=[50,50,50], hiddensizes=[200],
    topktags=false, opt=MyAdaGrad(0.01), evaliter=100, outfile="parser.dat")
    info("WILL RUN $iter ITERATIONS")

    if !isdefined(parser, :model)
        initmodel!(parser, FeedForward, embed, topktags,
               nonlinear, sparsesizes, embedsizes, hiddensizes)
    end
    saver = ModelSaver(outfile)
    initmodel!(parser, LabelerFF, embed, topktags,
               nonlinear, sparsesizes, embedsizes, hiddensizes)

    projectivize!(trainsents, false)

    trainsamples = LExample[]
    oracletrees = State{T}[]
    for sent in trainsents
        s = State(sent, parser)
        exs = Array(State{T}, length(sent))
        # exs[i] should be the final State object
        # at which i'th token appeared at s0.
        while !isfinal(s)
            s = expandgold(s)[1]
            if s.top != 0
                exs[s.top] = s
            end
        end
        setheads!(s)
        push!(oracletrees, s)
        @assert all(i -> isdefined(exs, i), 1:length(exs))
        append!(trainsamples, map(gen_trainexample, exs))
    end
    @assert all(isfinal, oracletrees)
    samplesize = length(trainsamples)
    labelsize = targetsize(parser.labeler)

    i = 1
    testtrees = deepcopy(testsents)
    projectivize!(testtrees)
    testtrees = map(testtrees) do sent
        print("\r", i)
        i += 1
        s = State(sent, parser)
        while !isfinal(s)
            s = expandgold(s)[1]
        end
        s
    end

    info("OPTIMIZER: ", typeof(opt))
    info("BATCH SIZE: $batchsize LABEL SIZE: $labelsize")
    info("#BATCHES: $(div(samplesize, batchsize))")
    info("#SAMPLES: $(samplesize)")

    for i = 1:iter
        batch = sub(trainsamples, rand(1:samplesize, batchsize))
        preds = parser.labeler(map(b -> (b.w, b.t, b.l), batch))
        golds = map(s -> s.target, batch)
        correct = reduce(0, zip(argmax(preds.data, 1), golds)) do v, tup
            v + Int(tup[1] == tup[2])
        end
        accuracy = correct / batchsize
        loss = update!(opt, golds, preds)
        info("Epoch: $i\tLoss: $loss\tAcc: $accuracy")

        if i % evaliter == 0 && !isempty(testsents)
            info("Epoch: $i\tEvaluating labeler")
            @time res = assign!(parser.labeler, testtrees)
            res = convert(Vector{Vector{Int}}, res)
            uas, las = evaluate(parser, testtrees, res)

            info("Epoch: $i\tEvaluating pipeline")
            @time tree, labels = decode(LabelerFF, parser, testsents)
            uas, las = evaluate(parser, tree, labels)
            saver(parser, las) # decides whether to save based on LAS
        end
    end
end

# do pipeline job: unlabeled parser -> labeler
function decode{T}(::Type{LabelerFF}, parser::DepParser{T}, sents::Doc;
    batchsize=32, progbar=true)
    batches = 1:batchsize:length(sents)
    blabels = []
    res = []
    for k in batches
        batch = k:min(k+batchsize-1, endof(sents))
        ss = map(s -> State(s, parser), sub(sents, batch))
        map(setheads!, ss)
        parsegreedy!(parser, ss)
        labels = assign!(parser.labeler, ss)
        append!(res, ss)
        append!(blabels, labels)
    end
    res, blabels
end


function EclipseDetector(arg0::PVCoordinatesProvider, arg1::jdouble, arg2::OneAxisEllipsoid)
    return EclipseDetector((PVCoordinatesProvider, jdouble, OneAxisEllipsoid), arg0, arg1, arg2)
end

function g(obj::EclipseDetector, arg0::SpacecraftState)
    return jcall(obj, "g", jdouble, (SpacecraftState,), arg0)
end

function get_occulted(obj::EclipseDetector)
    return jcall(obj, "getOcculted", PVCoordinatesProvider, ())
end

function get_occulted_radius(obj::EclipseDetector)
    return jcall(obj, "getOccultedRadius", jdouble, ())
end

function get_occulting(obj::EclipseDetector)
    return jcall(obj, "getOcculting", OneAxisEllipsoid, ())
end

function get_total_eclipse(obj::EclipseDetector)
    return jcall(obj, "getTotalEclipse", jboolean, ())
end

function with_penumbra(obj::EclipseDetector)
    return jcall(obj, "withPenumbra", EclipseDetector, ())
end

function with_umbra(obj::EclipseDetector)
    return jcall(obj, "withUmbra", EclipseDetector, ())
end


module Types

using Base: UUID

#using Pkg.Types: Context
#uuid(ctx::Context,name::AbstractString) = ctx.env.project.deps[name]

function uuid(name::String)
    for key in keys(Base.loaded_modules)
        if key.name==name
            return key.uuid
        end
    end
end

uuid(name::Symbol) = uuid(String(name))
uuid(name::Module) = uuid(nameof(name))
#uuid(name::Union{Module,Symbol,AbstractString}) = uuid(Context(),name)

# uuid(ctx::Context,name::Symbol) = uuid(ctx,String(name))
# uuid(ctx::Context,name::Module) = uuid(ctx,nameof(name))
# uuid(name::Union{Module,Symbol,AbstractString}) = uuid(Context(),name)

const CONFIG_DIR = homedir() * "/.demenet/"

function setconfigdir(dir::String)
    global CONFIG_DIR = dir
end

demefile(uuid::UUID) = CONFIG_DIR * "/demes/$uuid"
keydir(uuid::UUID) = CONFIG_DIR * "/keys/$uuid/"
datadir(uuid::UUID) = CONFIG_DIR * "/data/$uuid/"
#datadir(uuid::UUID,app::AbstractString) = CONFIG_DIR * "/apps/$app/$uuid/"

abstract type AbstractID end

struct ID <: AbstractID
    id::Union{BigInt,Nothing}
end

function Base.string(id::ID; length=nothing, kwargs...)
    str = string(id.id; kwargs...)
    if length!=nothing
        len = Base.length(str)
        @assert len<=length
        pre = "0"^(length-len)
    else
        pre = ""
    end
    return "$pre$str"
end

ID(str::AbstractString; kwargs...) = ID(parse(BigInt,str; kwargs...))

import Base.Dict

Dict(id::ID) = Dict("id"=>string(id,base=16))

ID(dict::Dict) = ID(dict["id"],base=16)

import Base.Vector

Vector{UInt8}(id::ID; kwargs...) = Vector{UInt8}(string(id; kwargs...))
ID(bytes::Vector{UInt8}; kwargs...) = ID(String(copy(bytes)); kwargs...)

Base.:(==)(a::ID,b::ID) = a.id==b.id
Base.hash(a::ID,h::UInt) = hash(a.id,hash(:ID,h))
Base.in(a::ID,b::ID) = a==b

struct DemeID <: AbstractID
    uuid::UUID
    id::ID
end

DemeID(uuid::UUID,id::BigInt) = DemeID(uuid,ID(id))

Base.:(==)(a::DemeID,b::DemeID) = a.uuid==b.uuid && a.id==b.id
Base.hash(a::DemeID,h::UInt) = hash(a.id,hash(a.uuid,hash(:DemeID, h)))

# Do I need a base hash also here?

Base.in(a::DemeID,b::DemeID) = a==b

### The Notary and Cypher could actually be defined with package where the implementation sits in. I could introduce AbstractNotary, AbstractCypher and just add a constructor CypherSuite{UUID}(AbstractNotary) to give me the right definition.

### Could have multiple signatures as one wishes

end


using ImageView, Images
#

cloud = load("/home/travis/Documents/Mael Engine/sprites/cloud.png")
cloud2 = load("/home/travis/Documents/Mael Engine/sprites/cloud2.png")
LeftPlayer = load("/home/travis/Documents/Mael Engine/sprites/leftplayer.png")
MiddlePlayer = load("/home/travis/Documents/Mael Engine/sprites/middleplayer.png")
img = fill(RGBA(0.65625,0.796875,1,1), 500, 800)

# ==============================================================================
# This draws a line to a picture!
# ==============================================================================
function line(image, x1, y1, x2, y2, color)
        dx, dy = x2-x1,  y2-y1

        for x in x1:x2
              y = y1 + dy * (x - x1) / dx
              image[ round(Int32, y), round(Int32, x) ] = color
        end
end
# ==============================================================================
# This draws a rectrangle to a picture!
# ==============================================================================
function rect(image, left, top, width, height, color)
    for y in 1:height, x in 1:width
        pixel(image, top+y, left+x, color )
    end
end
# ==============================================================================
# This draws a rectrangle to a picture! 1="centered" 2= topCenter
# ==============================================================================
function drawSprite(image, sprite, x, y, position=1)
    # width, height = size(image)
    SpHeight, SpWidth = size(sprite)
    if position==1
        cx,cy = round(Int32, SpWidth/2), round(Int32, SpHeight/2)
    elseif  position==2
        cx,cy = round(Int32, SpWidth/2), SpHeight
    elseif position==3
        cx,cy = round(Int32, SpWidth/2), 0
    end
    left, top = x-cy, y-cx

    for y in 1:SpWidth,  x in 1:SpHeight
        pixel(image, left+x, top+y, sprite[ x, y ] )
    end
end
# ==============================================================================
# This draws a pixel with Alpha chanel
# ==============================================================================
function pixel(image, X, Y, color )
    a = alpha(color)
    width, height = size(image)
    if X>0 && X < width && Y>0 && Y < height
        image[ X,Y ] = image[ X,Y ]*(1-a) + (color*a)
    end
end

rect(img, 200, 400, 150, 25, RGBA(0.65,0,0.5, 0.5) )
drawSprite(img, LeftPlayer, 400, 274, 2)
drawSprite(img, MiddlePlayer, 401, 477, 2)
rect(img, 0, 0, 50, 50, RGBA(0.65,0,0.5, 0.5) )

rect(img, 300, 410, 150, 25, RGBA(0.72,0.65,0.5, 0.8) )
# line(img, 100, 200, 250, 275, RGB(0,0.3,0.5))
drawSprite(img, cloud, 100, 100)backup
drawSprite(img, cloud, 100, 600)

drawSprite(img, cloud2, 100, 400)
drawSprite(img, cloud, 150, 200)
imshow(img)

#  type Player
#  	sprite::Sprite
#  	velocity::Vector2f
#  end


## Main loop to extract in e2e the DBSCAN parameters.
## The list of votable is selected directly from the directory
##
## That version is to run in parallel the optimization

## OPTIONS
# -s : first centile to consider in the vot files (0-100)
# -e : last centile to consider in the vot files (0-100)
# -m : metafile for all split

using DataFrames, Query
using CSV, Glob, Dates
using Statistics, Random
using Printf

rootdir =  ENV["GAIA_ROOT"]

push!(LOAD_PATH,"$rootdir/run/src")
using GaiaClustering

## directory
wdir    = "$rootdir/e2e_products"
votdir  = "$wdir/votable"
plotdir = "$wdir/plotSelect"
ocdir   = "$wdir/oc"

## load a liist of votable and update the file if done
## add results
##

function readlist_votable(filelist::String)
    vot = DataFrames.copy(CSV.read(filelist))
    return(vot)
end

function getdata(filevot)
    voname = filevot

    data       = read_votable(voname)
    df         = filter_data(data, [0,2100])
    dfcart     = add_cartesian(df)
    blck       = [[1,2,3],[4,5], [6,7,8]]
    wghtblck   = [4.0,5.0,1.0]
    norm       = "identity"

    dfcartnorm , scale8 = normalization_PerBlock(dfcart, blck, wghtblck , norm, false)
    return(df, dfcart , dfcartnorm)
end

## to check if done and record
## check and updt if votname analyzed. If not done return false
function updt_votcompleted(fileres, votname , cycletot=1, flag= 0 , onlycheck=true)
    let
        if  onlycheck
            if !isfile(fileres)
                return(0, false)
            else
                res = CSV.read(fileres, delim=";" , DataFrame)
                if votname in res.votname
                    x = @from i in res begin
                        @where i.votname == votname
                        @select i
                        @collect DataFrame
                    end
                    return(x, true)
                else
                    return(0, false)
                end
            end
            ## UPDTE
        else
            if !isfile(fileres)
                res = DataFrame(votname=votname, cycle=cycletot, flag=flag)
                CSV.write(fileres,res,delim=';')
                println("## $fileres created...")
                return(res,true)
            else
                res = CSV.read(fileres, delim=";", DataFrame)
                newrow = DataFrame(votname=votname,cycle=cycletot, flag=flag)
                append!(res,newrow)
                CSV.write(fileres,res,delim=';')
                return(res,true)
            end
        end
    end
end
## check for blacklist
##
function read_blacklist(blackname)
    if isfile(blackname)
        df= DataFrames.copy(CSV.read(blackname, delim=";"))
        blacklist= df.votname
    else
        blacklist= [""]
    end

    return(blacklist)
end

#######################
## Main loop
##
function main(filelist, metafile, prefile)
    let
        wd= pwd() ; nfile= size(filelist)[1] ; totalTime= 0.
        println("## Starting main loop using optimization with cycles...")
        println("## It can be very long but will be resumed to the last reduced file.")
        println("## Working directory: $wd")
        println("## $nfile files to analyze...")

        m= read_params(metafile, false)
        m.plotdir= plotdir
        m.ocdir= ocdir
        m.prefile= prefile
        fileres= "$(m.prefile).done.csv"

        # read a possible votname blacklist
        blackname= "blacklist.csv"
        blacklist= read_blacklist(blackname)
        println("## blacklist read...")

        for i in 1:nfile
            votname = filelist[i]
            res, votfound= updt_votcompleted(fileres, votname, 0, 0, true)

            ## test blacklist
            if votname in blacklist
                println("##")
                println("## $votname in blacklist...")
                println("##")
                votfound= true
            end

            if !votfound
                tstart= now()
                println("###########################")
                println("## Starting with $votname")
                println("## Starting at $tstart")

                df , dfcart , dfcartnorm = getdata(votdir*"/"*votname)
                m.votname= votname
                cycle, flag= cycle_extraction(df, dfcart, m)
                res,  votfound= updt_votcompleted(fileres, votname , cycle, flag, false)

                tend= now()
                println("## Ending at $tend")
                println("## number of cycle: $cycle , flag:$flag ")
                println("##########################")
                println("##")

                duration= Dates.value(tend-tstart) / (1000*3600)
                totalTime += duration
                meanTime= totalTime / i
                ETA= meanTime * (nfile-i) / 24
                nleft= nfile-i
                ETAstr= @sprintf("%3.3f", ETA) ; durationstr= @sprintf("%3.3f", duration)
                @printf("## %s \n",specialstr("Duration: $durationstr hours","YELLOW"))
                @printf("## %s \n",specialstr("ETA: $ETAstr days","YELLOW"))
                @printf("## %s \n",specialstr("Votable done: $votname","YELLOW"))
                @printf("## %s \n",specialstr("Files analyzed: $i","YELLOW"))
                @printf("## %s \n",specialstr("Files to go: $nleft","YELLOW"))
                println("##\n##")
            end
        end
    end
    print("## Main loop done.")
end
###############################################################################
let
    cd(votdir)
    votlist= glob("*.vot")
    cd(wdir)

    istart= 0 ; iend= 100
    metafile= "configAll.ext"

    for i in 1:length(ARGS)
        if ARGS[i] == "-s"
            istart= parse(Int, ARGS[i+1])
        end
        if ARGS[i] == "-e"
            iend= parse(Int, ARGS[i+1])
        end
        if ARGS[i] == "-m"
            metafile= ARGS[i+1]
        end
    end

    if istart < 0 istart= 0 end
    if iend > 100 iend= 100 end

###############
    header_extract()
    m= read_params(metafile, false)
    prefile= "$(m.prefile)-$istart-$iend"

    nfile= length(votlist)
    i1= convert(Int,floor(nfile*istart/100))
    if i1== 0 i1= 1 end

    if iend < 100
        i2= convert(Int,floor(nfile*iend/100)-1)
    else
        i2= convert(Int,floor(nfile*iend/100))
    end

    println("## DBSCAN optimization starting")
    println("## split files: $prefile")
    println("##")

    votsublist= votlist[i1:i2]
    main(votsublist, metafile, prefile)
    println("##\n## END of split optimization for $prefile \n##")
end


#!/usr/bin/env julia

#######
# CLI #
#######
import Celeste.ArgumentParse
parser = Celeste.ArgumentParse.ArgumentParser()
ArgumentParse.add_argument(
    parser,
    "--ais-output",
    help="Directory containing source-specific AIS sample files",
)
ArgumentParse.add_argument(
    parser,
    "--output-dir",
    help="Directory to output results dataframes",
)
ArgumentParse.add_argument(
    parser,
    "--truth-csv",
    help="True file (coadd, or prior generated file)",
)
ArgumentParse.add_argument(
    parser,
    "--vb-csv",
    help="VB inference file for comparison",
)
ArgumentParse.add_argument(
    parser,
    "--photo-csv",
    help="Photo inference file for comparison"
)
parsed_args = ArgumentParse.parse_args(parser, ARGS)
println(parsed_args)
println("======= scoring MCMC results ============")

#######################
# Analysis Output Dir #
#######################
import JLD, CSV
using DataFrames, StatsBase
import Celeste: AccuracyBenchmark, Model, MCMC

if haskey(parsed_args, "output-dir")
  output_dir = parsed_args["output-dir"]
else
  output_dir = "./mcmc_result_dataframes"
end
println(" ... saving output to ", output_dir)
if !isdir(output_dir)
  mkdir(output_dir)
end

#######################################################################
# collect MCMC files and summarize them into a results dataframe      #
#######################################################################
if haskey(parsed_args, "ais-output")
    ais_output_dir = parsed_args["ais-output"]
else
    ais_output_dir = "ais-output/synthetic-run-10-31"
end
ais_out = filter!(r"\.jld$", readdir(ais_output_dir))
results = [JLD.load(joinpath(ais_output_dir, f))["res"] for f in ais_out];
results = [res for res in results if !res["is_sky_bad"]]
@printf " ... found %d results files in directory %s \n" length(results) ais_output_dir

#####################################################
# Compute prob star/gal (based on model evidence    #
#####################################################
function compute_pstar(res; prior_is_star=.28) #[.28, .72])
    # is_star = [.28, .72] vs [.999, .001] vs [.5, .5]
    type_chain  = zeros(length(res["gal_bootstrap"]))
    lnprob_a    = log(prior_is_star)
    lnprob_nota = log(1.-prior_is_star)
    for n in 1:length(type_chain)
        # normalizing constant is ln p(data | star)
        # so p(star | data) \propto p(data | star) p(star)
        lnprob_star = res["star_bootstrap"][n] + lnprob_a
        lnprob_gal  = res["gal_bootstrap"][n] + lnprob_nota
        lnsum = Model.logsumexp([lnprob_star, lnprob_gal]) # normalize
        type_chain[n] = lnprob_star - lnsum
    end
    ave_pstar = Model.logsumexp(type_chain) - log(length(type_chain))
    return ave_pstar
end

for res in results
  res["ave_pstar"] = compute_pstar(res; prior_is_star=.28)
end

# must correct for off-by-pixel value
star_summary, gal_summary, results_df = MCMC.consolidate_samples(results;
    summarize=median)
#results_df[:dec] -= .396 / 3600.
#results_df[:ra]  -= .397 / 3600.

######################################################################
# Compute the per chain R Hats (TODO put these in the alg itself)    #
######################################################################
function compute_ss_rhat(sdf; num_chains=25, num_samps_per_chain=25)
    chains = []
    si = 1
    for i in 1:num_chains
        endi = si + num_samps_per_chain - 1
        #@printf "chunked %d : %d \n" si endi
        push!(chains, Array(sdf[si:endi,10:10]))
        si += num_samps_per_chain
    end
    return MCMC.potential_scale_reduction_factor(chains)
end

star_psrf = mean(vcat([compute_ss_rhat(res["star_samples"])
                       for res in results]...), 2)
gal_psrf  = mean(vcat([compute_ss_rhat(res["gal_samples"])
                       for res in results]...), 2)

######################################################################
# Load comparison data frames                                        #
######################################################################
if haskey(parsed_args, "truth-csv")
  truth_file = parsed_args["truth-csv"]
else
  truth_file = expanduser("~/Proj/Celeste.jl/benchmark/accuracy/output/prior_6328dda483.csv")
end
println(" ... loaded truth dataframe from ", truth_file)
truth = AccuracyBenchmark.read_catalog(truth_file)

if haskey(parsed_args, "vb-csv")
  vb_file = parsed_args["vb-csv"]
else
  vb_file = expanduser("~/Proj/Celeste.jl/benchmark/accuracy/output/prior_6328dda483_synthetic_6c24c1c9ad_predictions_342302e66a.csv")
end
println(" ... loaded VB comparison DF from ", vb_file)
vbdf = AccuracyBenchmark.read_catalog(vb_file)

if haskey(parsed_args, "photo-csv")
  photo_file = parsed_args["photo-csv"]
else
  println(" ... no photo file passed in --- loading VB as dummy ")
  photo_file = vb_file
end
photodf = AccuracyBenchmark.read_catalog(photo_file)

############################################################################
# Match prediction dataframes first --- throw away the same sources as VB  #
############################################################################
matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(
    truth, [vbdf, results_df, photodf]) #, tol=.396/3600*4)
@printf " ... found %d matched sources for comparison \n" size(matched_truth, 1)
truth = matched_truth
vbdf = matched_prediction_dfs[1]
results_df = matched_prediction_dfs[2]
photodf = matched_prediction_dfs[3]
@printf "Found %d nan r fluxes in the VB dataframe\n" sum(isnan.(vbdf[:flux_r_nmgy]))

#bad_idx = isnan.(vbdf[:flux_r_nmgy])
#truth = truth[.~bad_idx,:]
#vbdf = vbdf[.~bad_idx,:]
#results_df = results_df[.~bad_idx,:]
#photodf = photodf[.~bad_idx,:]

############################################
# Compare MCMC results samples to truth    #
############################################
function get_percentile_df(truth, sample_list)

    STDERR_COLUMNS = Set([
        :log_flux_r_stderr,
        :color_ug_stderr,
        :color_gr_stderr,
        :color_ri_stderr,
        :color_iz_stderr,
    ])

    # store these values --- easier that way
    truth[:log_flux_r] = log.(truth[:flux_r_nmgy])

    # go through each truth source, match it to a sample in the results list
    _, _, sample_summary = MCMC.consolidate_samples(sample_list; summarize=mean)
    #sample_summary[:dec] -= .396 / 3600.
    #sample_summary[:ra]  -= .396 / 3600.
    samp_pos = convert(Array, sample_summary[[:ra, :dec]])

    function get_percentile_column(column_name)
        percentiles, names = []  , []
        for i in 1:size(truth, 1)

            # match this truth position to the appropriate set of samples
            pos_i = convert(Array, truth[[:ra, :dec]][i, :])
            dists = sum(broadcast(-, samp_pos, pos_i).^2, 2)
            near_dist, near_idx = findmin(dists)

            # grab samples of appropriate source type
            sdict = results[near_idx]
            pstar = exp(sdict["ave_pstar"])
            samps = (pstar > .5) ? sdict["star_samples"] : sdict["gal_samples"]

            # truth percentile
            true_val = truth[column_name][i]
            true_percentile = sum(true_val .> sort(samps[column_name])) / float(size(samps, 1))

            push!(percentiles, true_percentile)
            push!(names, column_name)
        end
        return percentiles, names
    end

    # assemble dataframe of percentile values
    column_names = [:log_flux_r, :color_ug, :color_gr, :color_ri, :color_iz]
    res = [get_percentile_column(c) for c in column_names]
    pers = vcat([r[1] for r in res]...)
    names = vcat([r[2] for r in res]...)
    return DataFrame([names, pers], [:name, :percentile])
end

###############################################
# Compute scores for comparison data frames   #
###############################################
scores = AccuracyBenchmark.score_predictions(truth, [vbdf, results_df])
println("Comparing vb (first) to MCMC (second)")
println(repr(scores))

# uncertainties
vb_udf = AccuracyBenchmark.get_uncertainty_df(truth, vbdf);
vb_uscore = AccuracyBenchmark.score_uncertainty(vb_udf);

mc_udf = AccuracyBenchmark.get_uncertainty_df(truth, results_df);
mc_uscore = AccuracyBenchmark.score_uncertainty(mc_udf);
mc_perdf = get_percentile_df(truth, results)

println("VB Uncertainty")
println(vb_uscore)
println("MC Uncertainty")
println(mc_uscore)

# output to output dir
CSV.write(joinpath(output_dir, "uscore_vb.csv"), vb_uscore)
CSV.write(joinpath(output_dir, "uscore_mc.csv"), mc_uscore)
CSV.write(joinpath(output_dir, "udf_vb.csv"), vb_udf)
CSV.write(joinpath(output_dir, "udf_mc.csv"), mc_udf)
CSV.write(joinpath(output_dir, "perdf_mc.csv"), mc_perdf)

########################################################################
# Compute bootstrap resampled uncertainties for prediction estimates   #
########################################################################
function score_predictions_bootstrap(truth::DataFrame,
                                     prediction_dfs::Vector{DataFrame};
                                     num_bootstrap=1000,
                                     lo=2.5,
                                     hi=97.5)
    matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(truth, prediction_dfs)

    # in each sample, resample matched truth and matched pred dataframe rows
    scoredfs = []
    for bsamp in 1:num_bootstrap
        nsamps = size(matched_truth,1)
        bidx = [rand(1:nsamps) for n in 1:nsamps]
        mtruth = matched_truth[bidx,:]
        mpreds = [m[bidx,:] for m in matched_prediction_dfs]

        error_dfs = [AccuracyBenchmark.get_error_df(mtruth, predictions) for predictions in mpreds]
        @assert length(prediction_dfs) <= 2
        sdf = AccuracyBenchmark.get_scores_df(
            matched_truth,
            error_dfs[1],
            length(error_dfs) > 1 ? Nullable(error_dfs[2]) : Nullable{DataFrame}(),
        )
        sdf[:sample_i] = ones(Int64, size(sdf,1))*bsamp
        push!(scoredfs, sdf)
    end

    # assemble hi and lo values in the bootstrap samples
    sdf = scoredfs[1]
    boot_dict = Dict(:first_lo => [],
                     :first_hi => [])
                     #:field => [])
    if :second in names(sdf)
        boot_dict[:second_lo] = []
        boot_dict[:second_hi] = []
    end

    nrows = size(sdf, 1)
    for r in 1:nrows
        first_vals  = [sdf[r,:first] for sdf in scoredfs]
        push!(boot_dict[:first_lo], percentile(first_vals, lo))
        push!(boot_dict[:first_hi], percentile(first_vals, hi))
        if :second in names(sdf)
            second_vals = [sdf[r,:second] for sdf in scoredfs]
            push!(boot_dict[:second_lo], percentile(second_vals, lo))
            push!(boot_dict[:second_hi], percentile(second_vals, hi))
        end
        #push!(boot_dict[:field], sdf[r,:field])
    end

    scoremean = AccuracyBenchmark.score_predictions(truth, prediction_dfs)
    score_cis = DataFrame(boot_dict)
    return hcat(scoremean, score_cis), vcat(scoredfs...)
end

lo, hi = 2.5, 97.5
scoredf, scoredf_boot = score_predictions_bootstrap(truth, [vbdf, results_df]);
println("Bootstrap scoredf:")
println(scoredf)
CSV.write(joinpath(output_dir, "scoredf.csv"), scoredf)
CSV.write(joinpath(output_dir, "scoredf_boot.csv"), scoredf_boot)

##############################
# look at star ROC curve     #
##############################
#matched_truth, matched_prediction_dfs = AccuracyBenchmark.match_catalogs(
#    truth, [vbdf, results_df, photodf]) #, tol=.396/3600*4)
#@printf " ... found %d matched sources for comparison \n" size(matched_truth, 1)

# look at ROC curve for pstar
true_star = matched_truth[:is_star]
pstar_vb  = matched_prediction_dfs[1][:is_star]
pstar_mc  = matched_prediction_dfs[2][:is_star]
pstar_df = DataFrame(Dict(:true_star => true_star,
                          :pstar_vb => pstar_vb,
                          :pstar_mc => pstar_mc))
CSV.write(joinpath(output_dir, "pstardf.csv"), pstar_df)
CSV.write(joinpath(output_dir, "matched_truth.csv"), matched_truth)
CSV.write(joinpath(output_dir, "matched_vb.csv"), matched_prediction_dfs[1])
CSV.write(joinpath(output_dir, "matched_mc.csv"), matched_prediction_dfs[2])
CSV.write(joinpath(output_dir, "matched_photo.csv"), matched_prediction_dfs[3])


"""
NARP fileformat
See EUROCONTROL NEST Manual for NARP fileformat description

Example Narp file format
8214
AGGA;AUKI/GWAUNARU'U;-8.6983333333;160.6783333333;0;0;0;0
AGGE;BALALAE;-6.9833333333;155.8850000000;0;0;0;0
AGGH;HONIARA/HENDERSON;-9.4316666667;160.0533333333;0;0;0;0
AGGJ;AVU AVU;-9.8683333333;160.4100000000;0;0;0;0
AGGK;KIRA KIRA;-10.4500000000;161.8966666667;0;0;0;0
AGGL;GRACIOSA BAY;-10.7233333333;165.7800000000;0;0;0;0
AGGM;MUNDA;-8.3250000000;157.2633333333;0;0;0;0
AGGN;GIZO/NUSATAPE;-8.1016666667;156.8350000000;0;0;0;0
AGGP;PARASI;-9.6416666667;161.4252777778;0;0;0;82
"""
module Narp

export read

using Format
using CSV
using Dates
using DataFrames

const fileformat = Dict(1=>String, 2=>String, 3=>Float64, 4=>Float64,
5=>String, 6=>String, 7=>String, 8=>String)

const header_format = ["AIRPORT_ID", "AIRPORT_NAME", "LAT_DEG", "LON_DEG",
"TIS", "TRS", "TAXITIME", "ALTITUDE_FL"]

function read(file)
    df = CSV.read(file, delim=";", types=fileformat, header=header_format,
    copycols=true, datarow=2)
    reformat!(df)
    return df
end

function reformat!(df)
    df[:,:TEMP] = string2int.(df[:,:TIS])
    select!(df, Not(:TIS))
    df[:,:TIS] =  df[:,:TEMP]
    select!(df, Not(:TEMP))

    df[:,:TEMP] = string2int.(df[:,:TRS])
    select!(df, Not(:TRS))
    df[:,:TRS] =  df[:,:TEMP]
    select!(df, Not(:TEMP))

    df[:,:TEMP] = string2int.(df[:,:TAXITIME])
    select!(df, Not(:TAXITIME))
    df[:,:TAXITIME] =  df[:,:TEMP]
    select!(df, Not(:TEMP))

    df[:,:TEMP] = string2int.(df[:,:ALTITUDE_FL])
    select!(df, Not(:ALTITUDE_FL))
    df[:,:ALTITUDE_FL] =  df[:,:TEMP]
    select!(df, Not(:TEMP))
end

function string2int(str::String)
    maybeint = tryparse(Int, str)
    if maybeint == nothing
        return missing
    elseif str == ""
        return missing
    else
        return maybeint
    end
end

end # module