# ----------------------------------------------------------------------------------- #
# Copyright (c) 2019 Varnerlab
# Robert Frederick School of Chemical and Biomolecular Engineering
# Cornell University, Ithaca NY 14850

# 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.
# ----------------------------------------------------------------------------------- #

function contains(string,token)
    return occursin(token,string)
end

function eye(number_of_rows,number_of_cols)
    return Matrix{Float64}(I,number_of_rows, number_of_cols)
end

# load the files -
searchdir(path,key) = filter(x->contains(x,key),readdir(path))

# Function to set some values for the weights, gene lengths etc -
function customize_data_dictionary(default_data_dictionary::Dict{String,Any}, host_type::Symbol)::Dict{String,Any}

  # make a deepcopy -
  customized_data_dictionary = deepcopy(default_data_dictionary)

  # set the gene lengths -
  gene_length_array = customized_data_dictionary["gene_coding_length_array"]
  gene_length_array[1] = 744.0    # clssrA
  gene_length_array[2] = 711.0    # deGFPssrA
  gene_length_array[3] = 720.0    # S28
  gene_length_array[4] = 720.0    # S70 => doesn't matter, gene expression = 0

  # set the protein lengths -
  protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
  protein_coding_length_array[1] = 248.0  # clssrA
  protein_coding_length_array[2] = 237.0  # deGFPssrA
  protein_coding_length_array[3] = 240.0  # S28
  protein_coding_length_array[4] = 240.0  # S70 => doesn't matter, translation = 0

  # set ICs -
  initial_condition_array = customized_data_dictionary["initial_condition_array"]
  initial_condition_array[1] = 0.001  # clssrA gene
  initial_condition_array[2] = 0.008  # deGFP gene
  initial_condition_array[3] = 0.0015 # S28 gene
  initial_condition_array[4] = 0.0    # no S70 gene, just S70 protein in the extract
  #initial_condition_array[11] = 0.020 # sigma28 protein in muM
  initial_condition_array[12] = 0.035 # S70 protein in muM

  # setup the W's -
  control_parameter_dictionary = customized_data_dictionary["control_parameter_dictionary"]
  control_parameter_dictionary["W_clssrA_RNAP"] = 0.000014
  control_parameter_dictionary["W_clssrA_sigma_28"] = 1.0
  control_parameter_dictionary["W_deGFPssrA_RNAP"] = 0.000014
  control_parameter_dictionary["W_deGFPssrA_sigma_70"] = 10.0
  control_parameter_dictionary["W_deGFPssrA_clssrA"] = 100.0
  control_parameter_dictionary["W_sigma_28_RNAP"] = 0.00014
  control_parameter_dictionary["W_sigma_28_sigma_70"] = 100.0
  control_parameter_dictionary["W_sigma_28_clssrA"] = 0.1
  control_parameter_dictionary["W_sigma_70_RNAP"] = 0.0

  # setup the binding parameters -
  bP = readdlm("bP-Opt.dat")
  binding_parameter_dictionary = customized_data_dictionary["binding_parameter_dictionary"]
  bp_symbol_name_array = String[]
  push!(bp_symbol_name_array, "n_clssrA_sigma_28")
  push!(bp_symbol_name_array, "K_clssrA_sigma_28")
  push!(bp_symbol_name_array, "n_deGFPssrA_sigma_70")
  push!(bp_symbol_name_array, "K_deGFPssrA_sigma_70")
  push!(bp_symbol_name_array, "n_deGFPssrA_clssrA")
  push!(bp_symbol_name_array, "K_deGFPssrA_clssrA")
  push!(bp_symbol_name_array, "n_sigma_28_sigma_70")
  push!(bp_symbol_name_array, "K_sigma_28_sigma_70")
  push!(bp_symbol_name_array, "n_sigma_28_clssrA")
  push!(bp_symbol_name_array, "K_sigma_28_clssrA")
  for (index,key_text) in enumerate(bp_symbol_name_array)

      # get value -
      value = bP[index]

      # cache -
      binding_parameter_dictionary[key_text] = value
  end

  # setup degradation_modifier_array -
  degradation_modifier_array = [
    0.0	    ;	# 1	clssrA
    0.0	    ;	# 2	deGFPssrA
    0.0	    ;	# 3	sigma_28
    0.0	    ;	# 4	sigma_70
    0.05    ;	# 5	mRNA_clssrA 0.0085
    0.05	;	# 6	mRNA_deGFPssrA 0.05
    0.05	;	# 7	mRNA_sigma_28
    1.0	    ;	# 8	mRNA_sigma_70
    30.0	;	# 9	protein_clssrA
    28.0	;	# 10	protein_deGFPssrA
    1.5	    ;	# 11	protein_sigma_28
    1.0	    ;	# 12	protein_sigma_70
  ]
  customized_data_dictionary["degradation_modifier_array"] = degradation_modifier_array
  biophysical_constants_dictionary = customized_data_dictionary["biophysical_constants_dictionary"]
  species_symbol_type_array = customized_data_dictionary["species_symbol_type_array"]
  protein_coding_length_array = customized_data_dictionary["protein_coding_length_array"]
  gene_coding_length_array = customized_data_dictionary["gene_coding_length_array"]

  # get gene IC -
  idx_gene = findall(x->x==:gene,species_symbol_type_array)
  gene_abundance_array = initial_condition_array[idx_gene]

  # Dilution degrdation matrix -
  dilution_degradation_matrix = build_dilution_degradation_matrix(biophysical_constants_dictionary,species_symbol_type_array,degradation_modifier_array)
  customized_data_dictionary["dilution_degradation_matrix"] = dilution_degradation_matrix

  # time constant modifier -
  time_constant_modifier_array = [
    0.0	                    ;	  # 1	clssrA
    0.0	                    ;	  # 2	deGFPssrA
    0.0	                    ;	  # 3	sigma_28
    0.0                     ;	  # 4	sigma_70
    0.5549555127597618      ;	  # 5	mRNA_clssrA
    0.9961201331095983      ;	  # 6	mRNA_deGFPssrA
    0.9573079006791378      ;	  # 7	mRNA_sigma_28
    2.47595196225463	    ;	  # 8	mRNA_sigma_70
    1.2340314042937286	    ;	  # 9	protein_clssrA
    2.0833904410438735 	    ;	  # 10	protein_deGFPssrA
    4.99999996459193	    ;	  # 11	protein_sigma_28
    2.47595196225463	    ;	  # 12	protein_sigma_70
  ]
  customized_data_dictionary["time_constant_modifier_array"] = time_constant_modifier_array

  # Precompute the translation parameters -
  translation_parameter_array = precompute_translation_parameter_array(biophysical_constants_dictionary, protein_coding_length_array, time_constant_modifier_array,host_type)
  customized_data_dictionary["translation_parameter_array"] = translation_parameter_array

  # Precompute the kinetic limit of transcription -
  transcription_kinetic_limit_array = precompute_transcription_kinetic_limit_array(biophysical_constants_dictionary, gene_coding_length_array, gene_abundance_array, time_constant_modifier_array, host_type)
  customized_data_dictionary["transcription_kinetic_limit_array"] = transcription_kinetic_limit_array

  # return -
  return customized_data_dictionary
end

function load_experimental_data_dictionary(base_path::String)

  # create experimental data dictionary -
  exp_data_dictionary = Dict{String,DataFrame}()

  # load data -
  # deGFP mRNA -
  deGFP_mRNA_path = "$(base_path)/deGFP-ssrA-mRNA-pooled.csv"
  deGFP_mRNA_data_table = CSV.read(deGFP_mRNA_path)

  # sigma28 mRNA -
  sigma28_mRNA_path = "$(base_path)/sigma28-mRNA-pooled.csv"
  sigma28_mRNA_data_table = CSV.read(sigma28_mRNA_path)

  # c1-ssrA mRNA -
  c1_ssrA_mRNA_path = "$(base_path)/c1-ssrA-mRNA-pooled.csv"
  c1_ssrA_mRNA_data_table = CSV.read(c1_ssrA_mRNA_path)

  # deGFP protein -
  deGFP_protein_path = "$(base_path)/deGFP-protein-pooled.csv"
  deGFP_protein_data_table = CSV.read(deGFP_protein_path)

  # package -------------------------------------------------- #
  exp_data_dictionary["deGFP_mRNA_data_table"] = deGFP_mRNA_data_table
  exp_data_dictionary["sigma28_mRNA_data_table"] = sigma28_mRNA_data_table
  exp_data_dictionary["c1_ssrA_mRNA_data_table"] = c1_ssrA_mRNA_data_table
  exp_data_dictionary["deGFP_protein_data_table"] = deGFP_protein_data_table
  return exp_data_dictionary
  # ---------------------------------------------------------- #
end

function sort_parameter_ensemble_array(parameter_ensemble_array::Array{Float64,2})

  # The last row is the error -
  error_array = parameter_ensemble_array[end,:]

  # what is the perm index?
  idx_sort = sortperm(error_array) # default: small to big

  # shuffle cols -
  sorted_parameter_array = parameter_ensemble_array[:,idx_sort]

  # return -
  return sorted_parameter_array
end

function calculate_fisher_information_matrix(scaled_sensitivity_array,measurement_weight_array,id_parameter_index_array)

  # Get the size of the system -
  (number_of_rows,number_of_cols) = size(scaled_sensitivity_array);

  # grab the id cols -
  id_sensitivity_array = scaled_sensitivity_array[:,id_parameter_index_array]

  # Create the FIM (fisher_information_matrix) -
  FIM = transpose(id_sensitivity_array)*measurement_weight_array*id_sensitivity_array

  # return the FIM to the caller -
  return FIM
end

function estimate_identifiable_parameters(scaled_sensitivity_array,epsilon)

  # Get the size of the system -
  (number_of_rows,number_of_cols) = size(scaled_sensitivity_array);
  X = zeros(number_of_rows,1)
  pset = Int64[]

  R = scaled_sensitivity_array

  for col_index = 1:number_of_cols

    # get mag of col -
    local_m_array = zeros(number_of_cols)
    for inner_col_index = 1:number_of_cols

      value = R[:,inner_col_index]'*R[:,inner_col_index]
      local_m_array[inner_col_index] = value[1]
    end

    # what is the maximum element?
    max_value = maximum(local_m_array)
    max_index = indmax(local_m_array)

    # check tolerance -
    if max_value>epsilon

      # Grab this parameter -
      push!(pset,max_index)

      # Grab this col -
      X = [X scaled_sensitivity_array[:,max_index]]

      # remove leading col if first time through -
      if (col_index == 1)
        X = X[:,2:end]
      end

      # Create local array -
      Shat=X*inv(X'*X)*X'*scaled_sensitivity_array

      # Update R -
      R = scaled_sensitivity_array - Shat
    end
  end

  return sort!(pset)
end

function calculate_sensitivity_array(path_to_senstivity_files,file_pattern,time_skip,data_dictionary)

  # what is my system dimension?
  number_of_states = data_dictionary["number_of_states"]

  # load the files -
  searchdir(path,key) = filter(x->contains(x,key),readdir(path))

  # block_dictionary -
  block_dictionary = Dict()

  # build file list -
  list_of_files = searchdir(path_to_senstivity_files,file_pattern)
  number_of_files = length(list_of_files)
  time_array = []
  for file_index = 1:number_of_files

    # Build path -
    path_to_data_file = path_to_senstivity_files*"/"*file_pattern*string(file_index)*".dat"

    # Load file -
    local_data_array = readdlm(path_to_data_file)

    # split -
    time_array = local_data_array[:,1]
    X = local_data_array[:,2:end]
    state_array = X[:,1:number_of_states]
    sensitivity_array = X[:,(number_of_states+1):end]
    scaled_sensitivity_block = scale_sensitivity_array(time_array,state_array,sensitivity_array,file_index,data_dictionary)

    # store the transpose -
    key_symbol = file_pattern*string(file_index)*".dat"
    block_dictionary[key_symbol] = transpose(scaled_sensitivity_block)
  end

  # what is my system dimension?
  number_of_timesteps = length(time_array)
  number_of_parameters = number_of_files

  # initialize -
  sensitivity_array = zeros(number_of_states,number_of_parameters)
  sample_time_array = Float64[]
  for time_step_index = 1:time_skip:number_of_timesteps

    time_value = time_array[time_step_index]
    push!(sample_time_array,time_value)

    local_sens_block = zeros(number_of_states,number_of_parameters)
    for parameter_index = 1:number_of_parameters

      # get the block from the dictionary -
      key_symbol = file_pattern*string(parameter_index)*".dat"
      block = block_dictionary[key_symbol]

      # grab the col -
      block_col = block[:,time_step_index]

      for state_index = 1:number_of_states
        local_sens_block[state_index,parameter_index] = block_col[state_index]
      end
    end

    # add block to s array -
    sensitivity_array = [sensitivity_array ; local_sens_block]
  end

  # cutoff leading block -
  sensitivity_array = sensitivity_array[(number_of_states+1):end,:]
  return (sample_time_array,sensitivity_array)
end

function calculate_average_scaled_sensitivity_array(path_to_senstivity_files,file_pattern,data_dictionary)

  # what is my system dimension?
  number_of_states = data_dictionary["number_of_states"]

  # initialize -
  average_scaled_sensitivity_array = zeros(number_of_states,1)

  # load the files -
  searchdir(path,key) = filter(x->contains(x,key),readdir(path))

  # build file list -
  list_of_files = searchdir(path_to_senstivity_files,file_pattern)
  number_of_files = length(list_of_files)
  for file_index = 1:number_of_files

    # Build path -
    path_to_data_file = path_to_senstivity_files*"/"*file_pattern*string(file_index)*".dat"

    # Load file -
    local_data_array = readdlm(path_to_data_file)

    # split -
    time_array = local_data_array[:,1]
    X = local_data_array[:,2:end]
    state_array = X[:,1:number_of_states]
    sensitivity_array = X[:,(number_of_states+1):end]
    scaled_sensitivity_array = scale_sensitivity_array(time_array,state_array,sensitivity_array,file_index,data_dictionary)

    # time average -
    average_sensitivity_col = time_average_array(time_array,scaled_sensitivity_array)

    # grab -
    average_scaled_sensitivity_array = [average_scaled_sensitivity_array average_sensitivity_col]
  end

  # trim leading col -
  average_scaled_sensitivity_array = average_scaled_sensitivity_array[:,2:end]
  return average_scaled_sensitivity_array
end

function time_average_array(time_array,data_array)

  # what is the delta T?
  delta_time = (time_array[end] - time_array[1])

  # initialize -
  average_array = Float64[]

  # what is the size of the array?
  (number_of_timesteps,number_of_states) = size(data_array)
  for state_index = 1:number_of_states

    # grab the data column -
    data_col = data_array[:,state_index]

    # average -
    average_value = (1/delta_time)*trapz(time_array,data_col)

    # push -
    push!(average_array,average_value)
  end

  return average_array

end

function scale_sensitivity_array(time_array,state_array,sensitivity_array,parameter_index,data_dictionary)

  # what is small?
  epsilon = 1e-6

  # initialize -
  (number_of_timesteps,number_of_states) = size(state_array)
  scaled_sensitivity_array = zeros(number_of_timesteps,number_of_states)

  # What is the nominal parameter value?
  parameter_name_mapping_array = data_dictionary["parameter_name_mapping_array"]
  parameter_name = parameter_name_mapping_array[parameter_index]

  # build the total parameter dictionary -
  binding_parameter_dictionary = data_dictionary["binding_parameter_dictionary"]
  control_parameter_dictionary = data_dictionary["control_parameter_dictionary"]
  txtl_parameter_dictionary = data_dictionary["biophysical_constants_dictionary"]
  total_parameter_dictionary = merge(binding_parameter_dictionary,control_parameter_dictionary,txtl_parameter_dictionary)

  # Grab the default value -
  default_parameter_value = total_parameter_dictionary[parameter_name]

  # main loop -
  for (time_index,time_value) in enumerate(time_array)

    for state_index = 1:number_of_states

      state_value = state_array[time_index,state_index]
      if (state_value<epsilon)
        state_value = epsilon
      end

      old_value = sensitivity_array[time_index,state_index]
      new_value = old_value*(default_parameter_value/state_value)
      scaled_sensitivity_array[time_index,state_index] = new_value
    end
  end

  return scaled_sensitivity_array
end

function calculate_jacobian(time,state_array,data_dictionary)

  # what is the size of the system?
  number_of_states = length(state_array)

  # calculate each row of the jacobian -
  jacobian_array = zeros(1,number_of_states)
  for (state_index,state_value) in enumerate(state_array)

    jacobian_row = calculate_jacobian_row(time,state_array,state_index,data_dictionary)
    jacobian_array = [jacobian_array  ; transpose(jacobian_row)]
  end

  jacobian_array = jacobian_array[2:end,:]
  return jacobian_array
end

function calculate_bmatrix(time,state_array,data_dictionary)

  # what is the size of the system?
  parameter_name_mapping_array = data_dictionary["parameter_name_mapping_array"]
  number_of_parameters = length(parameter_name_mapping_array)
  number_of_states = length(state_array)

  # calculate each row of the jacobian -
  b_array = zeros(number_of_parameters,1)
  for (state_index,state_value) in enumerate(state_array)

    bmatrtix_row = calculate_bmatrix_row(time,state_array,state_index,data_dictionary)
    b_array = [b_array bmatrtix_row]
  end

  b_array = b_array[:,2:end]
  return transpose(b_array)
end

function calculate_bmatrix_row(time,state_array,balance_index,data_dictionary)

  # define some constants -
  epsilon = 1e-6
  delta = 0.01

  # parameter name dictionary -
  parameter_name_mapping_array = data_dictionary["parameter_name_mapping_array"]
  binding_parameter_dictionary = data_dictionary["binding_parameter_dictionary"]
  control_parameter_dictionary = data_dictionary["control_parameter_dictionary"]
  txtl_parameter_dictionary = data_dictionary["biophysical_constants_dictionary"]
  number_of_parameters = length(parameter_name_mapping_array)

  # how many binding parameters do we have?
  number_of_binding_parameters = length(binding_parameter_dictionary)
  number_of_control_parameters = length(control_parameter_dictionary)

  # create a mega dictionary -
  total_parameter_dictionary = merge(binding_parameter_dictionary,control_parameter_dictionary,txtl_parameter_dictionary)

  # create delta parameter array -
  parameter_delta_array = Float64[]
  for (parameter_index,parameter_name) in enumerate(parameter_name_mapping_array)

    # Grab the default value -
    default_parameter_value = total_parameter_dictionary[parameter_name]

    # state perturbation -
    peturbed_parameter_value = default_parameter_value*(delta);

    #@show (peturbed_parameter_value,default_parameter_value)

    # check -
    if (peturbed_parameter_value<epsilon)
      peturbed_parameter_value = epsilon
    end

    # capture -
    push!(parameter_delta_array,(peturbed_parameter_value))
  end

  # Create the diag array -
  diag_delta_array = Diagonal(vec(parameter_delta_array))

  # Create bVec -
  f_nominal = calculate_balances(time,vec(state_array),data_dictionary)

  # estimate the perturbed balances -
  rhs_delta_array = Float64[]
  for (parameter_index,parameter_name) in enumerate(parameter_name_mapping_array)

    # copy -
    local_data_dictionary = deepcopy(data_dictionary)

    # Grab the default value -
    default_parameter_value = total_parameter_dictionary[parameter_name]

    # update the state -
    perturbed_parameter_array = zeros(number_of_parameters)
    for local_index = 1:number_of_parameters
      if (parameter_index == local_index)
        perturbed_parameter_array[parameter_index] = default_parameter_value*(1+delta);
      else
        local_parameter_name = parameter_name_mapping_array[local_index]
        perturbed_parameter_array[local_index] = total_parameter_dictionary[local_parameter_name]
      end
    end

    if (parameter_index<=number_of_binding_parameters)

      # we are in the binding section -
      local_data_dictionary["binding_parameter_dictionary"][parameter_name] = perturbed_parameter_array[parameter_index]
    elseif (parameter_index>number_of_binding_parameters && parameter_index<=(number_of_binding_parameters+number_of_control_parameters))
      # we are in the control section -
      local_data_dictionary["control_parameter_dictionary"][parameter_name] = perturbed_parameter_array[parameter_index]
    else
      # we are in the bar parameter section -
      local_data_dictionary[parameter_name] = perturbed_parameter_array[parameter_index]
    end

    # calculate the perturbed balances -
    f_perturbed = calculate_balances(time,vec(state_array),local_data_dictionary)
    f_perturbed = f_perturbed[balance_index] - f_nominal[balance_index]

    # capture -
    push!(rhs_delta_array,f_perturbed)
  end

  # calculate the bmatrix row -
  bmatrix_row = diag_delta_array\rhs_delta_array

  # return -
  return bmatrix_row
end

function finite_diff_jacobian(time,state_array,data_dictionary)

  # define some constants -
  epsilon = 1e-6
  delta = 0.05
  number_of_states = length(state_array)

  # initialize -
  jacobian_array = zeros(number_of_states,number_of_states)

  # nominal -
  f_nominal = calculate_balances(time,vec(state_array),data_dictionary)

  #@show state_array

  for row_index = 1:number_of_states
    for col_index = 1:number_of_states

      perturbed_state_array = zeros(number_of_states)
      for perturbation_index = 1:number_of_states

        if (col_index == perturbation_index)
          perturbed_state_array[col_index] = state_array[col_index]*(1+delta)
        else
          perturbed_state_array[perturbation_index] = state_array[perturbation_index]
        end
      end

      #@show perturbed_state_array

      # calculate the balances -
      f_perturbed = calculate_balances(time,vec(perturbed_state_array),data_dictionary)

      # calculate the entry -
      perturbation_size = state_array[col_index]*delta
      if (perturbation_size<epsilon)
        perturbation_size = epsilon
      end
      jacobian_array[row_index,col_index] = (f_perturbed[row_index] - f_nominal[row_index])/(perturbation_size)
    end
  end

  return jacobian_array
end

function calculate_jacobian_row(time,state_array,balance_index,data_dictionary)

  # define some constants -
  epsilon = 1e-6
  delta = 0.001
  number_of_states = length(state_array)

  # Create the delta state array -
  state_delta_array = Float64[]
  for (state_index,state_value) in enumerate(state_array)

    # state perturbation -
    peturbed_state = state_value*(delta);

    # check -
    if (peturbed_state<epsilon)
      peturbed_state = epsilon
    end

    # capture -
    push!(state_delta_array,peturbed_state)
  end

  # Create the diag array -
  diag_delta_array = Diagonal(vec(state_delta_array))

  # Create bVec -
  f_nominal = calculate_balances(time,vec(state_array),data_dictionary)

  # estimate the perturbed balances -
  rhs_delta_array = Float64[]
  for (state_index,state_value) in enumerate(state_array)

    # update the state -
    perturbed_state_array = zeros(number_of_states)

    for local_index = 1:number_of_states
      if (state_index == local_index)
        perturbed_state_array[state_index] = state_value*(1+delta);
      else
        perturbed_state_array[local_index] = state_array[local_index]
      end
    end

    # calculate the perturbed balances -
    f_perturbed = calculate_balances(time,vec(perturbed_state_array),data_dictionary)
    f_perturbed_delta = f_perturbed[balance_index] - f_nominal[balance_index]

    # capture -
    push!(rhs_delta_array,f_perturbed_delta)
  end

  # calculate the jacobian row -
  jacobian_row = diag_delta_array\rhs_delta_array

  # return -
  return jacobian_row
end

function build_biophysical_dictionary(path_to_biophysical_constants_file::String, host_type::Symbol)::Dict{String,Any}

    # check the path - is it legit?
    check_file_existence(path_to_biophysical_constants_file)

    # Load the model dictionary -
    model_dictionary = JSON.parsefile(path_to_biophysical_constants_file)

    # check the host type in the biophysical file - need to match
    file_host_type = Symbol(model_dictionary["host_type"])
    if (file_host_type != host_type)
        throw(error("inconsistent host_type. Expected $(host_type) found $(file_host_type)"))
    end

    # initialize -
    biophysical_dictionary = Dict{String,Any}()

    # convert -
    list_of_constant_dictionaries = model_dictionary["biophysical_constants"]
    for (key_string, local_dictionary) in list_of_constant_dictionaries

        # get the value -
        value = parse(Float64, local_dictionary["value"])

        # cache -
        biophysical_dictionary[key_string] = value
    end

    # return -
    return biophysical_dictionary
end

function trapz(x::Array{Float64}, y::Array{Float64})

    # Trapezoidal integration rule
    local n = length(x)
    if (length(y) != n)
        error("Vectors 'x', 'y' must be of same length")
    end

    r = 0
    if n == 1; return r; end
    for i in 2:n
        r += (x[i] - x[i-1]) * (y[i] + y[i-1])
    end
    #= correction -h^2/12 * (f'(b) - f'(a))
    ha = x[2] - x[1]
    he = x[end] - x[end-1]
    ra = (y[2] - y[1]) / ha
    re = (y[end] - y[end-1]) / he
    r/2 - ha*he/12 * (re - ra)
    =#
    return r/2
end


"""
    PrecipFormation

Storage for tendencies due to precipitation formation

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct PrecipFormation{FT}
    θ_liq_ice_tendency::FT
    qt_tendency::FT
    qr_tendency::FT
    qs_tendency::FT
end

"""
    EntrDetr

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct EntrDetr{FT}
    "Dynamical entrainment"
    ε_dyn::FT
    "Dynamical detrainment"
    δ_dyn::FT
    "Turbulent entrainment"
    ε_turb::FT
    "Horizontal eddy-diffusivity"
    K_ε::FT
end

"""
    GeneralizedEntr

A general set of variables entrainment might depend on.

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct GeneralizedEntr{FT}
    "updraft condensate (liquid water + ice)"
    q_cond_up::FT
    "environment condensate (liquid water + ice)"
    q_cond_en::FT
    "updraft vertical velocity"
    w_up::FT
    "environment vertical velocity"
    w_en::FT
    "updraft buoyancy"
    b_up::FT
    "environment buoyancy"
    b_en::FT
    "environment tke"
    tke::FT
    "updraft momentum divergence"
    dMdz::FT
    "updraft momentum"
    M::FT
    "updraft area fraction"
    a_up::FT
    "environment area fraction"
    a_en::FT
    "pressure plume spacing"
    R_up::FT
    "updraft relative humidity"
    RH_up::FT
    "environment relative humidity"
    RH_en::FT
    "maximum updraft area"
    max_area::FT
    "vertical coordinate"
    zc_i::FT
    "Model time step"
    Δt::FT
end

Base.eltype(::GeneralizedEntr{FT}) where {FT} = FT

struct MDEntr end  # existing model
struct NNEntr end

"""
    GradBuoy

Environmental buoyancy gradients.

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct GradBuoy{FT}
    "environmental vertical buoyancy gradient"
    ∂b∂z::FT
    "vertical buoyancy gradient in the unsaturated part of the environment"
    ∂b∂z_unsat::FT
    "vertical buoyancy gradient in the saturated part of the environment"
    ∂b∂z_sat::FT
end

abstract type AbstractEnvBuoyGradClosure end
struct BuoyGradMean <: AbstractEnvBuoyGradClosure end
struct BuoyGradQuadratures <: AbstractEnvBuoyGradClosure end

"""
    EnvBuoyGrad

Variables used in the environmental buoyancy gradient computation.

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct EnvBuoyGrad{FT, EBC <: AbstractEnvBuoyGradClosure}
    "temperature in the saturated part"
    t_sat::FT
    "vapor specific humidity  in the saturated part"
    qv_sat::FT
    "total specific humidity in the saturated part"
    qt_sat::FT
    "potential temperature in the saturated part"
    θ_sat::FT
    "liquid ice potential temperature in the saturated part"
    θ_liq_ice_sat::FT
    "virtual potential temperature gradient in the non saturated part"
    ∂θv∂z_unsat::FT
    "total specific humidity gradient in the saturated part"
    ∂qt∂z_sat::FT
    "liquid ice potential temperature gradient in the saturated part"
    ∂θl∂z_sat::FT
    "reference pressure"
    p0::FT
    "cloud fraction"
    en_cld_frac::FT
    "specific volume"
    alpha0::FT
end
function EnvBuoyGrad(::EBG; t_sat::FT, bg_kwargs...) where {FT <: Real, EBG <: AbstractEnvBuoyGradClosure}
    return EnvBuoyGrad{FT, EBG}(; t_sat, bg_kwargs...)
end

"""
    MixLen

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct MixLen{FT}
    "minimum length number"
    min_len_ind::Int
    "mixing length"
    mixing_length::FT
    "length ratio"
    ml_ratio::FT
end

"""
    MinDisspLen

Minimum dissipation model

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef struct MinDisspLen{FT}
    "height"
    z::FT
    "obukhov length"
    obukhov_length::FT
    "surface TKE values"
    tke_surf::FT
    "u star - surface velocity scale"
    ustar::FT
    "turbulent Prandtl number"
    Pr::FT
    "reference pressure"
    p0::FT
    "vertical buoyancy gradient struct"
    ∇b::GradBuoy{FT}
    "env shear"
    Shear²::FT
    "environment turbulent kinetic energy"
    tke::FT
    "Updraft tke source"
    b_exch::FT
end

Base.@kwdef mutable struct PrecipVariables
    precipitation_model::String = "default_precipitation_model"
    mean_rwp::Float64 = 0
    mean_swp::Float64 = 0
    cutoff_precipitation_rate::Float64 = 0
end
function PrecipVariables(namelist, grid::Grid)

    precipitation_model = parse_namelist(
        namelist,
        "microphysics",
        "precipitation_model";
        default = "None",
        valid_options = ["None", "cutoff", "clima_1m"],
    )

    if !(precipitation_model in ["None", "cutoff", "clima_1m"])
        error("precipitation model not recognized")
    end

    return PrecipVariables(; precipitation_model)
end

mutable struct UpdraftVariables{A1}
    n_updrafts::Int
    cloud_base::A1
    cloud_top::A1
    cloud_cover::A1
    updraft_top::A1
    lwp::Float64
    iwp::Float64
    function UpdraftVariables(nu, namelist, grid::Grid)
        n_updrafts = nu

        # cloud and precipitation diagnostics for output
        cloud_base = zeros(nu)
        cloud_top = zeros(nu)
        cloud_cover = zeros(nu)
        updraft_top = zeros(nu)

        lwp = 0.0
        iwp = 0.0

        A1 = typeof(cloud_base)
        return new{A1}(n_updrafts, cloud_base, cloud_top, cloud_cover, updraft_top, lwp, iwp)
    end
end

Base.@kwdef mutable struct GridMeanVariables{PS}
    param_set::PS
    lwp::Float64
    iwp::Float64
    cloud_base::Float64
    cloud_top::Float64
    cloud_cover::Float64
    EnvThermo_scheme::String
end
function GridMeanVariables(namelist, grid::Grid, param_set::PS) where {PS}
    lwp = 0.0
    iwp = 0.0

    cloud_base = 0.0
    cloud_top = 0.0
    cloud_cover = 0.0

    EnvThermo_scheme = parse_namelist(namelist, "thermodynamics", "sgs"; default = "mean")

    return GridMeanVariables(; param_set, lwp, iwp, cloud_base, cloud_top, cloud_cover, EnvThermo_scheme)
end

Base.@kwdef mutable struct EnvironmentVariables
    cloud_base::Float64 = 0
    cloud_top::Float64 = 0
    cloud_cover::Float64 = 0
    lwp::Float64 = 0
    iwp::Float64 = 0
    EnvThermo_scheme::String = "default_EnvThermo_scheme"
end
function EnvironmentVariables(namelist, grid::Grid)
    # TODO: EnvThermo_scheme is repeated in GridMeanVariables
    EnvThermo_scheme = parse_namelist(namelist, "thermodynamics", "sgs"; default = "mean")
    return EnvironmentVariables(; EnvThermo_scheme)
end

struct EnvironmentThermodynamics
    quadrature_order::Int
    quadrature_type::String
    function EnvironmentThermodynamics(namelist, grid::Grid)
        quadrature_order = parse_namelist(namelist, "thermodynamics", "quadrature_order"; default = 3)
        quadrature_type = parse_namelist(namelist, "thermodynamics", "quadrature_type"; default = "gaussian")
        return new(quadrature_order, quadrature_type)
    end
end

# Stochastic entrainment/detrainment closures:
struct NoneClosureType end
struct LogNormalClosureType end
struct SDEClosureType end

# Stochastic differential equation memory
Base.@kwdef mutable struct sde_struct{T}
    u0::Float64
    dt::Float64
end

# SurfaceMoninObukhovDry:
#     Needed for dry cases (qt=0). They have to be initialized with nonzero qtg for the
#     reference profiles. This surface subroutine sets the latent heat flux to zero
#     to prevent errors due to nonzero qtg in vars such as the obukhov_length.
# SurfaceSullivanPatton
#     Not fully implemented yet. Maybe not needed - Ignacio
struct SurfaceNone end
struct SurfaceFixedFlux end
struct SurfaceFixedCoeffs end
struct SurfaceMoninObukhov end
struct SurfaceMoninObukhovDry end
struct SurfaceSullivanPatton end

Base.@kwdef mutable struct SurfaceBase{T}
    zrough::Float64 = 0
    interactive_zrough::Bool = false
    Tsurface::Float64 = 0
    qsurface::Float64 = 0
    shf::Float64 = 0
    lhf::Float64 = 0
    cm::Float64 = 0
    ch::Float64 = 0
    cq::Float64 = 0
    bflux::Float64 = 0
    windspeed::Float64 = 0
    ustar::Float64 = 0
    rho_qtflux::Float64 = 0
    rho_hflux::Float64 = 0
    rho_uflux::Float64 = 0
    rho_vflux::Float64 = 0
    obukhov_length::Float64 = 0
    Ri_bulk_crit::Float64 = 0
    ustar_fixed::Bool = false
    ref_params::NamedTuple = NamedTuple()
end

function SurfaceBase(::Type{T}; namelist::Dict, ref_params) where {T}
    Ri_bulk_crit = namelist["turbulence"]["Ri_bulk_crit"]
    return SurfaceBase{T}(; Ri_bulk_crit, ref_params)
end

struct ForcingBaseType end
struct ForcingNone end
struct ForcingStandard end
struct ForcingDYCOMS_RF01 end
struct ForcingLES end

struct RadiationBaseType end
struct RadiationNone end
struct RadiationStandard end
struct RadiationDYCOMS_RF01 end
struct RadiationLES end

Base.@kwdef mutable struct LESData
    "Start time index of LES"
    imin::Int = 0
    "End time index of LES"
    imax::Int = 0
    "Path to LES stats file used to drive SCM"
    les_filename::String = nothing
    "Drive SCM with LES data from t = [end - t_interval_from_end_s, end]"
    t_interval_from_end_s::Float64 = 6 * 3600.0
    "Length of time to average over for SCM initialization"
    initial_condition_averaging_window_s::Float64 = 3600.0
end

"""
    ForcingBase

LES-driven forcing

$(DocStringExtensions.FIELDS)
"""
Base.@kwdef mutable struct ForcingBase{T}
    "Boolean specifying whether Coriolis forcing is applied"
    apply_coriolis::Bool = false
    "Boolean specifying whether subsidence forcing is applied"
    apply_subsidence::Bool = false
    "Coriolis parameter"
    coriolis_param::Float64 = 0
    "Momentum relaxation timescale"
    nudge_tau::Float64 = 0.0
end

force_type(::ForcingBase{T}) where {T} = T

Base.@kwdef mutable struct RadiationBase{T}
    divergence::Float64 = 0
    alpha_z::Float64 = 0
    kappa::Float64 = 0
    F0::Float64 = 0
    F1::Float64 = 0
end

rad_type(::RadiationBase{T}) where {T} = T

Base.@kwdef mutable struct CasesBase{T}
    case::T
    casename::String = "default_casename"
    inversion_option::String = "default_inversion_option"
    les_filename::String = "None"
    Sur::SurfaceBase
    Fo::ForcingBase
    Rad::RadiationBase
    rad_time::StepRangeLen = range(10, 360; length = 36) .* 60
    rad::AbstractMatrix{Float64} = zeros(1, 1)
    lhf0::Float64 = 0
    shf0::Float64 = 0
    LESDat::Union{LESData, Nothing} = nothing
end

CasesBase(case::T; kwargs...) where {T} = CasesBase{T}(; case = case, casename = string(nameof(T)), kwargs...)

mutable struct EDMF_PrognosticTKE{N_up, A1, EBGC, EC, SDES, UPVAR}
    Ri_bulk_crit::Float64
    zi::Float64
    n_updrafts::Int
    asp_label::String
    extrapolate_buoyancy::Bool
    surface_area::Float64
    max_area::Float64
    aspect_ratio::Float64
    tke_ed_coeff::Float64
    tke_diss_coeff::Float64
    static_stab_coeff::Float64
    minimum_area::Float64
    Precip::PrecipVariables
    UpdVar::UPVAR
    EnvVar::EnvironmentVariables
    EnvThermo::EnvironmentThermodynamics
    area_surface_bc::A1
    w_surface_bc::A1
    h_surface_bc::A1
    qt_surface_bc::A1
    pressure_plume_spacing::A1
    prandtl_number::Float64
    wstar::Float64
    entr_surface_bc::Float64
    detr_surface_bc::Float64
    dt_max::Float64
    sde_model::SDES
    bg_closure::EBGC
    entr_closure::EC
    function EDMF_PrognosticTKE(namelist, grid::Grid, param_set::PS) where {PS}
        # get values from namelist
        prandtl_number = namelist["turbulence"]["prandtl_number_0"]
        Ri_bulk_crit = namelist["turbulence"]["Ri_bulk_crit"]
        zi = 0.0

        # Set the number of updrafts (1)
        n_updrafts = parse_namelist(namelist, "turbulence", "EDMF_PrognosticTKE", "updraft_number"; default = 1)

        pressure_func_drag_str = parse_namelist(
            namelist,
            "turbulence",
            "EDMF_PrognosticTKE",
            "pressure_closure_drag";
            default = "normalmode",
            valid_options = ["normalmode", "normalmode_signdf"],
        )

        asp_label = parse_namelist(
            namelist,
            "turbulence",
            "EDMF_PrognosticTKE",
            "pressure_closure_asp_label";
            default = "const",
        )

        extrapolate_buoyancy =
            parse_namelist(namelist, "turbulence", "EDMF_PrognosticTKE", "extrapolate_buoyancy"; default = true)

        # Get values from namelist
        # set defaults at some point?
        surface_area = namelist["turbulence"]["EDMF_PrognosticTKE"]["surface_area"]
        max_area = namelist["turbulence"]["EDMF_PrognosticTKE"]["max_area"]
        # entrainment parameters
        # pressure parameters
        aspect_ratio = namelist["turbulence"]["EDMF_PrognosticTKE"]["aspect_ratio"]

        # mixing length parameters
        tke_ed_coeff = namelist["turbulence"]["EDMF_PrognosticTKE"]["tke_ed_coeff"]
        tke_diss_coeff = namelist["turbulence"]["EDMF_PrognosticTKE"]["tke_diss_coeff"]
        static_stab_coeff = namelist["turbulence"]["EDMF_PrognosticTKE"]["static_stab_coeff"]
        # Need to code up as namelist option?
        minimum_area = 1e-5

        # Create the class for precipitation
        Precip = PrecipVariables(namelist, grid)

        # Create the updraft variable class (major diagnostic and prognostic variables)
        UpdVar = UpdraftVariables(n_updrafts, namelist, grid)

        # Create the environment variable class (major diagnostic and prognostic variables)
        EnvVar = EnvironmentVariables(namelist, grid)
        # Create the class for environment thermodynamics
        EnvThermo = EnvironmentThermodynamics(namelist, grid)

        # Near-surface BC of updraft area fraction
        area_surface_bc = zeros(n_updrafts)
        w_surface_bc = zeros(n_updrafts)
        h_surface_bc = zeros(n_updrafts)
        qt_surface_bc = zeros(n_updrafts)
        pressure_plume_spacing = zeros(n_updrafts)

        # Initialize SDE parameters
        dt = parse_namelist(namelist, "time_stepping", "dt_min"; default = 1.0)
        closure = parse_namelist(
            namelist,
            "turbulence",
            "EDMF_PrognosticTKE",
            "stochastic",
            "closure";
            default = "none",
            valid_options = ["none", "lognormal", "sde"],
        )
        closure_type = if closure == "none"
            NoneClosureType
        elseif closure == "lognormal"
            LogNormalClosureType
        elseif closure == "sde"
            SDEClosureType
        else
            error("Something went wrong. Invalid stochastic closure type '$closure'")
        end
        sde_model = sde_struct{closure_type}(u0 = 1, dt = dt)

        bg_type = parse_namelist(
            namelist,
            "turbulence",
            "EDMF_PrognosticTKE",
            "env_buoy_grad";
            default = "mean",
            valid_options = ["mean", "quadratures"],
        )
        bg_closure = if bg_type == "mean"
            BuoyGradMean()
        elseif bg_type == "quadratures"
            BuoyGradQuadratures()
        else
            error("Something went wrong. Invalid environmental buoyancy gradient closure type '$bg_type'")
        end

        # entr closure
        entr_type = parse_namelist(
            namelist,
            "turbulence",
            "EDMF_PrognosticTKE",
            "entrainment";
            default = "moisture_deficit",
            valid_options = ["moisture_deficit", "NN"],
        )
        entr_closure = if entr_type == "moisture_deficit"
            MDEntr()
        elseif entr_type == "NN"
            NNEntr()
        else
            error("Something went wrong. Invalid entrainment type '$entr_type'")
        end
        EC = typeof(entr_closure)

        wstar = 0
        entr_surface_bc = 0
        detr_surface_bc = 0
        dt_max = 0
        A1 = typeof(area_surface_bc)
        EBGC = typeof(bg_closure)
        SDES = typeof(sde_model)
        UPVAR = typeof(UpdVar)
        return new{n_updrafts, A1, EBGC, EC, SDES, UPVAR}(
            Ri_bulk_crit,
            zi,
            n_updrafts,
            asp_label,
            extrapolate_buoyancy,
            surface_area,
            max_area,
            aspect_ratio,
            tke_ed_coeff,
            tke_diss_coeff,
            static_stab_coeff,
            minimum_area,
            Precip,
            UpdVar,
            EnvVar,
            EnvThermo,
            area_surface_bc,
            w_surface_bc,
            h_surface_bc,
            qt_surface_bc,
            pressure_plume_spacing,
            prandtl_number,
            wstar,
            entr_surface_bc,
            detr_surface_bc,
            dt_max,
            sde_model,
            bg_closure,
            entr_closure,
        )
    end
end
parameter_set(obj) = obj.param_set
n_updrafts(edmf::EDMF_PrognosticTKE{N_up}) where {N_up} = N_up

struct State{P, A, T}
    prog::P
    aux::A
    tendencies::T
end


export UniformEnergyDistribution

"Initial uniform energy distribution."
struct UniformEnergyDistribution{N} <: InitialEnergyDistribution{N}
    "Specific energy."
    u::Float

    UniformEnergyDistribution{N}(u::Number) where {N} = @check(u > 0) && new(u)
end

Base.show(io::IO, ::Type{UniformEnergyDistribution{N}}) where {N} =
    print(io, "UniformEnergyDistribution")

function (distribution::UniformEnergyDistribution{N})(
    positions::AbstractVector{SVectorF{N}},
    mass_densities::AbstractVector{Float},
    eos::EquationOfState,
) where {N}
    @checkeq(length(positions), length(mass_densities))
    fill(distribution.u, length(positions))
end


function convert_func(op, T1, W1, T2, W2 = W1)
    typ1 = LLVM_TYPES[T1]
    typ2 = LLVM_TYPES[T2]
    vtyp1 = vtype(W1, typ1)
    vtyp2 = vtype(W2, typ2)
    instrs = """
    %res = $op $vtyp2 %0 to $vtyp1
    ret $vtyp1 %res
    """
    quote
        $(Expr(:meta, :inline))
        Vec(llvmcall($instrs, _Vec{$W1,$T1}, Tuple{_Vec{$W2,$T2}}, data(v)))
    end
end
function identity_func(W, T1, T2)
    vtyp1 = vtype(W, LLVM_TYPES[T1])
    instrs = """
    ret $vtyp1 %0
    """
    quote
        $(Expr(:meta, :inline))
        Vec(llvmcall($instrs, _Vec{$W,$T1}, Tuple{_Vec{$W,$T2}}, data(v)))
    end
end

@generated function Vec{W,F}(v::Vec{W,T}) where {W,F<:Union{Float32,Float64},T<:Integer}
    convert_func(T <: Signed ? "sitofp" : "uitofp", F, W, T)
end

@generated function Vec{W,T}(v::Vec{W,F}) where {W,F<:Union{Float32,Float64},T<:Integer}
    convert_func(T <: Signed ? "fptosi" : "fptoui", T, W, F)
end
@generated function Vec{W,T1}(v::Vec{W,T2}) where {W,T1<:Integer,T2<:Integer}
    sz1 = sizeof(T1)::Int; sz2 = sizeof(T2)::Int
    if sz1 < sz2
        convert_func("trunc", T1, W, T2)
    elseif sz1 == sz2
        identity_func(W, T1, T2)
    else
        convert_func(((T1 <: Signed) && (T2 <: Signed)) ? "sext" : "zext", T1, W, T2)
    end
end

@inline Base.float(v::Vec{W,I}) where {W, I <: Union{UInt64, Int64}} = Vec{W,Float64}(v)
@generated function Base.float(v::Vec{W,I}) where {W, I <: Integer}
    ex = if 8W ≤ REGISTER_SIZE
        :(Vec{$W,Float64}(v))
    else
        :(Vec{$W,Float32}(v))
    end
    Expr(:block, Expr(:meta, :inline), ex)
end
@inline Base.float(v::AbstractSIMD{W,T}) where {W,T <: Union{Float32,Float64}} = v
@inline Base.float(vu::VecUnroll) = VecUnroll(fmap(float, vu.data))

    # for bytes2 in (1,2,4)
    #     bytes2 == bytes1 && break
    #     llvmint2 = "i$(8bytes2)"
    #     jint2 = Symbol(:Int, 8bytes2)
    #     juint2 = Symbol(:UInt, 8bytes2)
    #     expr = :(convert_func("trunc", W, $llvmint2, $jint2, $llvmint, $jint))
    #     @eval @generated Vec{W,$jint2}(v::Vec{W,$jint}) where {W} = $expr
    #     expr = :(convert_func("trunc", W, $llvmint2, $juint2, $llvmint, $jint))
    #     @eval @generated Vec{W,$juint2}(v::Vec{W,$jint}) where {W} = $expr
    #     expr = :(convert_func("trunc", W, $llvmint2, $jint2, $llvmint, $juint))
    #     @eval @generated Vec{W,$jint2}(v::Vec{W,$juint}) where {W} = $expr
    #     expr = :(convert_func("trunc", W, $llvmint2, $juint2, $llvmint, $juint))
    #     @eval @generated Vec{W,$juint2}(v::Vec{W,$juint}) where {W} = $expr

    #     expr = :(convert_func("sext", W, $llvmint, $jint, $llvmint2, $jint2))
    #     @eval @generated Vec{W,$jint}(v::Vec{W,$jint2}) where {W} = $expr
    #     expr = :(convert_func("zext", W, $llvmint, $juint, $llvmint2, $jint2))
    #     @eval @generated Vec{W,$juint}(v::Vec{W,$jint2}) where {W} = $expr
    #     expr = :(convert_func("zext", W, $llvmint, $jint, $llvmint2, $juint2))
    #     @eval @generated Vec{W,$jint}(v::Vec{W,$juint2}) where {W} = $expr
    #     expr = :(convert_func("zext", W, $llvmint, $juint, $llvmint2, $juint2))
    #     @eval @generated Vec{W,$juint}(v::Vec{W,$juint2}) where {W} = $expr
    # end
    # expr = :(identity_func(W, $llvmint, $juint, $jint))
    # @eval @generated Vec{W,$juint}(v::Vec{W,$jint}) where {W} = $expr
    # expr = :(identity_func(W, $llvmint, $jint, $juint))
    # @eval @generated Vec{W,$jint}(v::Vec{W,$juint}) where {W} = $expr
# end

@generated function Vec{W,Float32}(v::Vec{W,Float64}) where {W}
    convert_func("fptrunc", Float32, W, Float64, W)
end
@generated function Vec{W,Float64}(v::Vec{W,Float32}) where {W}
    convert_func("fpext", Float64, W, Float32, W)
end

@inline Base.convert(::Vec{W,T}, s::T) where {W, T <: NativeTypes} = vbroadcast(Val{W}(), s)
@inline Base.convert(::Vec{W,T}, s::T) where {W, T <: Integer} = vbroadcast(Val{W}(), s)
@inline Base.convert(::Vec{W,T}, s::NativeTypes) where {W, T} = vbroadcast(Val{W}(), T(s))
@inline Base.convert(::Vec{W,T1}, s::T2) where {W, T1 <: Integer, T2 <: Union{Int8,UInt8,Int16,UInt16,Int32,UInt32,Int64,UInt64}} = vbroadcast(Val{W}(), s % T1)
@inline function Base.convert(::Type{T}, v::Vec{W,S}) where {T<:Number,S,W}
    if S <: T
        v
    else
        Vec{W,T}(v)
    end
end
@inline function Base.convert(::Type{U}, v::Vec{W,S}) where {N,W,T,U<:VecUnroll{N,W,T},S}
    VecUnroll{N}(convert(T, v))
end
@inline Base.convert(::Type{Vec{W,T}}, v::Vec{W,S}) where {T<:Number,S,W} = Vec{W,T}(v)
@inline Base.unsafe_trunc(::Type{I}, v::Vec{W,T}) where {W,I,T} = convert(Vec{W,I}, v)
@inline Base.:(%)(v::Vec{W,T}, ::Type{I}) where {W,I,T} = convert(Vec{W,I}, v)

@inline Base.convert(::Type{T}, v::Union{Mask,VecUnroll{<:Any, <:Any, Bool, <: Mask}}) where {T <: Union{Base.HWReal,Bool}} = ifelse(v, one(T), zero(T))
@inline Base.convert(::Type{<:AbstractSIMD{W,T}}, v::Union{Mask{W},VecUnroll{<:Any, W, Bool, <: Mask}}) where {W, T <: Union{Base.HWReal,Bool}} = ifelse(v, one(T), zero(T))
@inline Base.convert(::Type{Bit}, v::Union{Mask,VecUnroll{<:Any, <:Any, Bool, <: Mask}}) = v
@inline Base.convert(::Type{<:AbstractSIMD{W,Bit}}, v::Union{Mask{W},VecUnroll{<:Any, W, Bool, <: Mask}}) where {W} = v
# @inline Base.convert(::typeof{T}, v::T)

@generated function Base.reinterpret(::Type{T1}, v::Vec{W2,T2}) where {W2, T1 <: NativeTypes, T2}
    W1 = W2 * sizeof(T2) ÷ sizeof(T1)
    Expr(:block, Expr(:meta, :inline), :(reinterpret(Vec{$W1,$T1}, v)))
end
@generated function Base.reinterpret(::Type{Vec{W1,T1}}, v::Vec{W2,T2}) where {W1, W2, T1, T2}
    @assert sizeof(T1) * W1 == W2 * sizeof(T2)
    convert_func("bitcast", T1, W1, T2, W2)
end


#To Do: Make it possible to load from file

#Create AbstractSpacecraft type
abstract type AbstractSpacecraft end

#original Code definition of RigidBodyVehicle with rotational inertia
struct RigidBodyVehicle <: AbstractSpacecraft
  Name::String
  DryMass::Float64
  Fuel::Float64
  RefArea::Float64
  RefLengthLon::Float64
  RefLengthLat::Float64
  ReflecCoeff::Float64
  SRParea::Float64
  InertiaTensor::Matrix{Float64}
  InvInertiaTensor::Matrix{Float64}
  VehicleAeroDataBase::Type{<:AbstractVehicleDataBase}
  Actuators::Vector{Type{<:AbstractActuator}}
  MissionSystem::Type{<:AbstractMissionSystem}
  GuidanceSystem::Type{<:AbstractGuidanceSystem}
  ControlSystem::Type{<:AbstractControlSystem}
  NavigationSystem::Type{<:AbstractNavigationSystem}
  InitialState::SimState
  Trajectory::Vector{SimState}

  #Constructor for vehicle type
  function RigidBodyVehicle(;Name::String = "MySpacecraft", Drymass::Float64 = 1000.,
      Fuel::Float64 = 0., ReferenceArea::Float64 = 100., ReferenceLengthLongitude::Float64 = 23.,
      ReferenceLengthLatitude::Float64 = 12., ReflectionCoefficient::Float64 = 0.5,
      SRP_Area::Float64 = 20., InertiaTensor::Matrix{Float64} = diagm([10000., 10000., 10000.]),
      InverseInertiaTensor::Matrix{Float64} = inv(InertiaTensor),
      VehicleAeroDatabase::Type{<:AbstractVehicleDataBase} = VehicleAeroDataBase(),
      Actuators::Vector{Type{<:AbstractActuator}} = Vector{AbstractActuator}(),
      MissionSystem::Type{<:AbstractMissionSystem} = MissionSystem(),
      GuidanceSystem::Type{<:AbstractGuidanceSystem} = GuidanceSystem(),
      ControlSystem::Type{<:AbstractControlSystem} = ControlSystem(),
      NavigationSystem::Type{<:AbstractNavigationSystem} = NavigationSystem(),
      InitialState::SimState = SimState(Vector{Float64}(6), Vector{Float64}(3)),
      Trajectory::Vector{SimState} = Vector{SimState}())

      #Create Vehicle
      new(Name, Drymass, Fuel, ReferenceArea, ReferenceLengthLongitude, ReferenceLengthLatitude,
            ReflectionCoefficient, SRP_Area, InertiaTensor, InverseInertiaTensor,
            VehicleAeroDataBase, Actuators, MissionSystem, GuidanceSystem, ControlSystem,
            NavigationSystem, InitialState, Trajectory)
  end #End Constructor
end


module LoadCase

include("./Load.jl")

using SparseArrays

using ..Enums
using .Load

export LoadCaseSet,AbstractLoadCase

# export StaticCase,ModalCase,BucklingCase,TimeHistoryCase,ResponseSpectrumCase

export add_static_case!, add_modal_case!, add_buckling_case!, add_time_history_case!

export add_nodal_disp!,add_nodal_force!,
add_cable_distributed!,add_cable_strain!,add_cable_stress!,
add_link_distributed!,add_link_strain!,add_link_stress!,
add_beam_distributed!,add_beam_strain!
export set_modal_params!,set_buckling_params!,set_time_history_params!
export is_static, is_modal, is_buckling, is_time_history, is_response_spectrum, get_all_id

abstract type AbstractLoadCase end

mutable struct StaticCase <: AbstractLoadCase
    id::String
    hid::Int

    nl_type::String
    plc::String

    gfactor::Float64
    nodal_forces::Dict{String,NodalForce}
    nodal_disps::Dict{String,NodalDisp}
    beam_forces::Dict{String,BeamForce}
    quad_forces::Dict{String,QuadForce}

    P::Vector
    P̄::Vector

    StaticCase(id,hid,nl_type="1st",plc="",gfactor=0.)=new(
        string(id),
        hid,
        nl_type,
        plc,
        gfactor,
        Dict{String,NodalForce}(),
        Dict{String,NodalDisp}(),
        Dict{String,BeamForce}(),
        Dict{String,QuadForce}(),
        zeros(1),
        zeros(1))
end

mutable struct ModalCase <: AbstractLoadCase
    id::String
    hid::Int

    modal_type::String
    plc::String

    nev::Int
    tolev::Number
    maxiterev::Int

    P::Vector
    P̄::Vector

    ModalCase(id,hid,modal_type,plc="")=new(
        string(id),
        hid,
        modal_type,
        plc,
        6,
        1e-9,
        100,
        zeros(1),
        zeros(1))
end

mutable struct BucklingCase <: AbstractLoadCase
    id::String
    hid::Int

    plc::String

    gfactor::Float64
    nodal_forces::Dict{String,NodalForce}
    nodal_disps::Dict{String,NodalDisp}
    beam_forces::Dict{String,BeamForce}
    quad_forces::Dict{String,QuadForce}

    shift::Float64
    positive_only::Bool

    nev::Int
    tolev::Number
    maxiterev::Int

    P::Vector
    P̄::Vector

    BucklingCase(id,hid,nl_type="1st",plc="",gfactor=0.)=new(
        string(id),
        hid,
        plc,
        gfactor,
        Dict{String,NodalForce}(),
        Dict{String,NodalDisp}(),
        Dict{String,BeamForce}(),
        Dict{String,QuadForce}(),
        0.,
        false,
        6,
        1e-12,
        100,
        zeros(1),
        zeros(1))
end

mutable struct TimeHistoryCase <: AbstractLoadCase
    id::String
    hid::Int

    t::Array{Float64}
    f::Array{Float64}

    plc::String

    nodal_forces::Dict{String,NodalForce}
    nodal_disps::Dict{String,NodalDisp}
    beam_forces::Dict{String,BeamForce}
    quad_forces::Dict{String,QuadForce}

    algorithm::String
    α::Float64
    β::Float64
    γ::Float64
    θ::Float64
    modal_case::String

    P::Vector
    P̄::Vector
    TimeHistoryCase(id,hid,t,f,plc="")=new(
        string(id),
        hid,
        t,f,
        plc,
        Dict{String,NodalForce}(),
        Dict{String,NodalDisp}(),
        Dict{String,BeamForce}(),
        Dict{String,QuadForce}(),
        "newmark",
        0.85,
        0.25,
        0.5,
        1.4,
        "",
        zeros(1),
        zeros(1))
end

mutable struct ResponseSpectrumCase <: AbstractLoadCase
    id::String
    hid::Int

    t::Array{Float64}
    α::Array{Float64}

    modal_case::String

    plc::String

    dirfactors::Array{Float64}
end

mutable struct LoadCaseSet
    statics::Dict{String,StaticCase}
    modals::Dict{String,ModalCase}
    bucklings::Dict{String,BucklingCase}
    time_histories::Dict{String,TimeHistoryCase}
    response_spectrums::Dict{String,ResponseSpectrumCase}

    lc_tree::Array
    LoadCaseSet()=new(Dict{String,StaticCase}(),
    Dict{String,ModalCase}(),
    Dict{String,BucklingCase}(),
    Dict{String,TimeHistoryCase}(),
    Dict{String,ResponseSpectrumCase}(),
    [])
end

get_all_id(lcset::LoadCaseSet)=union(keys(lcset.statics),keys(lcset.modals),keys(lcset.bucklings),keys(lcset.time_histories),keys(lcset.response_spectrums))

"""
    add_static_case!(lcset,id,gfactor,lc_type,plc)
向lcset加入新静力工况
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `gfactor`: 自重乘数
- `nl_type`: 非线性类型，应为1st、2nd、3rd中之一
- `plc`: nl_type为2nd、3rd时可选的提供初始刚度的前一步工况
"""
function add_static_case!(lcset::LoadCaseSet,id,gfactor=0.;nl_type="1st",plc="")
    all_ids=get_all_id(lcset)
    if id in all_ids
        throw("load case with id "*string(id)*" already exists!")
    end
    if plc!="" && !(plc in keys(lcset.statics))
        throw("previous load case with id "*string(plc)*" doe not exists!")
    end
    hid=length(lcset.statics)+1
    lcset.statics[id]=StaticCase(id,hid,nl_type,plc,gfactor)
end

"""
    add_modal_case!(lcset,id,gfactor,lc_type,plc)
向lcset加入新模态工况
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `modal_type`: 工况类型，应为eigen、ritz中之一
- `plc`: 可选的提供初始刚度的前一步静力工况
"""
function add_modal_case!(lcset::LoadCaseSet,id;modal_type="eigen",plc="")
    all_ids=get_all_id(lcset)
    id=string(id)
    if id in all_ids
        throw("load case with id "*id*" already exists!")
    end
    if plc!="" && !(plc in keys(lcset.statics))
        throw("previous load case with id "*string(plc)*" doe not exists!")
    end
    hid=length(keys(lcset.modals))+1
    lcset.modals[id]=ModalCase(id,hid,modal_type,plc)
end

"""
    set_modal_params!(lcset::LoadCaseSet,lc;n=12,tol=1e-12,maxiter=300,method="eigen")
设置模态工况分析参数
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `n`: 模态阶数
- `tol`: 迭代收敛容差
- `maxiter`: 最大迭代数
- `method`: 求解模态方法，应为`eigen`或`ritz`之一
"""
function set_modal_params!(lcset::LoadCaseSet,lc;n=12,tol=1e-12,maxiter=300,modal_type="eigen")
    lc=string(lc)
    if !(lc in keys(lcset.modals))
        throw("Modal case with id "*lc*" doe not exists!")
    end
    lcset.modals[lc].nev=n
    lcset.modals[lc].tolev=tol
    lcset.modals[lc].maxiterev=maxiter
    lcset.modals[lc].modal_type=modal_type
end

"""
    add_buckling_case!(lcset,id,gfactor,lc_type,plc)
向lcset加入新屈曲工况
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `plc`: 可选的提供初始刚度的前一步静力工况
"""
function add_buckling_case!(lcset::LoadCaseSet,id;plc="")
    all_ids=get_all_id(lcset)
    if id in all_ids
        throw("load case with id "*string(id)*" already exists!")
    end
    if plc!="" && !(plc in keys(lcset.statics))
        throw("previous load case with id "*string(plc)*" doe not exists!")
    end
    hid=length(keys(lcset.modals))+1
    lcset.bucklings[id]=Buckling(id,hid,modal_type,plc)
end

"""
    set_buckling_params!(lcset::LoadCaseSet,lc;n=12,tol=1e-12,maxiter=300)
设定屈曲工况分析参数
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `n`: 模态阶数
- `tol`: 迭代收敛容差
- `maxiter`: 最大迭代数
"""
function set_buckling_params!(lcset::LoadCaseSet,lc;n=12,tol=1e-12,maxiter=300)
    lc=string(lc)
    if !lc in lcset.modals
        throw("Buckling case with id "*lc*" doe not exists!")
    end
    lcset.bucklings[lc].nev=n
    lcset.bucklings[lc].tolev=tol
    lcset.bucklings[lc].maxiterev=maxiter
    lcset.bucklings[lc].modal_method=method
end

"""
    add_time_history_case!(lcset::LoadCaseSet,id,t,f;plc="")
向lcset加入新时程工况
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `id`: 工况id
- `t`: 时间值
- `f`: 函数值
- `plc`: 可选的提供初始刚度的前一步静力工况
"""
function add_time_history_case!(lcset::LoadCaseSet,id,t,f;plc="")
    all_ids=get_all_id(lcset)
    id=string(id)
    plc=string(plc)
    if id in all_ids
        throw("Load case with id "*id*" already exists!")
    end
    if plc!="" && !(plc in keys(lcset.statics))
        throw("Previous load case with id "*plc*" doe not exists!")
    end
    hid=length(keys(lcset.modals))+1
    lcset.time_histories[id]=TimeHistoryCase(id,hid,t,f,plc)
end

"""
    set_time_history_params!(lcset::LoadCaseSet,lc,algorithm;α=0.85,β=0.25,γ=0.5,θ=1.4)
设定时程工况分析参数
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `algorithm`: 0-中心差分法； 1-Newmark-β法； 2-Wilson-Θ法；3-振型分解法
- `α`: 对HHT法的可选参数
- `β`: 对Newmark、Wilson法的可选参数
- `γ`: 对Newmark、Wilson法的可选参数
- `θ`: 对Wilson法的可选参数
- `modal_case`: 对振型分解法要求的模态工况
"""
function set_time_history_params!(lcset::LoadCaseSet,lc,algorithm;α=0.85,β=0.25,γ=0.5,θ=1.4,modal_case="")
    lc=string(lc)
    if !(lc in keys(lcset.time_histories))
        throw("Time history case with id "*lc*" doe not exists!")
    end
    if algorithm==0
        algor="central_diff"
    elseif algorithm==1
        algor="newmark"
    elseif algorithm==2
        algor="wilson"
    elseif algorithm==3
        algor="modal_decomp"
    else
        throw("算法不存在")
    end
    lcset.time_histories[lc].algorithm=algor
    lcset.time_histories[lc].α=α
    lcset.time_histories[lc].β=β
    lcset.time_histories[lc].γ=γ
    lcset.time_histories[lc].θ=θ
    lcset.time_histories[lc].modal_case=modal_case
end

"""
    add_nodal_disp!(lcset,lc,id,u1,u2,u3,r1,r2,r3;csys)
向lcset加入节点强制位移
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `id`: 节点id/组id
- `u1,u2,u3,r1,r2,r3`: 各自由度位移
# 可选参数
- `csys`: 参考坐标系，应为"global"或"local"
"""
function add_nodal_disp!(lcset::LoadCaseSet,lc,id,u1,u2,u3,r1,r2,r3;csys="global")
    lc=string(lc)
    if !(lc in keys(lcset.static))
        throw("Static case with id "*lc*" doesn't exists!")
    end
    disp=[u1,u2,u3,r1,r2,r3]
    nodal_disp=NodeDisp(id,disp,csys)
    lcset.statics[lc].nodal_disps[string(id)]=nodal_disp
end

"""
    add_nodal_force!(lcset,lc,id,f1,f2,f3,m1,m2,m3;csys)
向lcset加入节点集中力
# 参数
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `id`: 节点id
- `f1,f2,f3,m1,m2,m3`: 各自由度集中力
# 可选参数
- `csys`: 参考坐标系，应为"global"或"local"
"""
function add_nodal_force!(lcset::LoadCaseSet,lc,id,f1,f2,f3,m1,m2,m3;csys="global")
    lc=string(lc)
    cases=merge(lcset.statics,lcset.bucklings,lcset.time_histories)
    if !(lc in keys(cases))
        throw("Load case with id "*lc*" doesn't exists!")
    end
    id=string(id)
    force=Float64.([f1,f2,f3,m1,m2,m3])
    nodal_force=NodalForce(id,force,csys)
    cases[lc].nodal_forces[id]=nodal_force
end

function add_cable_distributed!(lcset::LoadCaseSet,lc,id,fi1,fi2,fi3,fj1,fj2,fj3;csys="local")
    lc=string(lc)
    if !(lc in keys(lcset.static))
        throw("static case with id "*lc*" doesn't exists!")
    end
    id=string(id)
    force=Float64.([fi1,fi2,fi3,fj1,fj2,fj3])
    idx=findfirst(x->x.id==id,loadcase.cable_forces)
    if csys=="local"
        if idx isa Nothing
            cable_force=LinkForce(id)
            cable_force.f=force
            push!(lcset.statics[hid].cable_forces,cable_force)
        else
            lcset.statics[hid].cable_force[idx].f+=force
        end
    else
        #pass
    end
end

"""
    add_beam_distributed!(lcset::LoadCaseSet,lc,id,fi1,fi2,fi3,mi1,mi2,mi3,fj1,fj2,fj3,mj1,mj2,mj3;csys)
向lcset加入beam单元分布力
# Arguments
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `id`: 单元id
- `fi1,fi2,fi3,mi1,mi2,mi3,fj1,fj2,fj3,mj1,mj2,mj3`: i,j端的分布力集度
# 可选参数
- `csys`: 参考坐标系，应为"global"或"local"
"""
function add_beam_distributed!(lcset::LoadCaseSet,lc,id,fi1,fi2,fi3,mi1,mi2,mi3,fj1,fj2,fj3,mj1,mj2,mj3;csys="local")
    lc=string(lc)
    if !(lc in keys(lcset.statics))
        throw("static case with id "*lc*" doesn't exists!")
    end
    id=string(id)
    force=Float64.([fi1,fi2,fi3,mi1,mi2,mi3,fj1,fj2,fj3,mj1,mj2,mj3])
    if csys=="local"
        if !(id in keys(lcset.statics[lc].beam_forces))
            beam_force=BeamForce(id)
            beam_force.f=force
            lcset.statics[lc].beam_forces[id]=beam_force
        else
            lcset.statics[lc].beam_forces[id].f+=force
        end
    else
        #pass
    end
end

"""
    add_beam_strain!(lcset,lc,val)
向lcset加入beam单元初应变
# Arguments
- `lcset::LoadCaseSet`: LoadCaseSet实例
- `lc`: 工况id
- `id`: 单元id
- `val`: 应变值
"""
function add_beam_strain!(lcset::LoadCaseSet,lc,id,val)
    lc=string(lc)
    if !(lc in keys(lcset.statics))
        throw("load case with id "*lc*" doesn't exists!")
    end
    if !(id in keys(lcset.statics[lc].beam_forces))
        beam_force=BeamForce(id)
        beam_force.s[1]=val
        beam_force.s[7]=val
        lcset.statics[lc].beam_forces[id]=beam_force
    else
        lcset.statics[lc].beam_forces[id].s[1]+=val
        lcset.statics[lc].beam_forces[id].s[7]+=val
    end
end

function add_link_stress!(lcset::LoadCaseSet)
end
is_static(loadcase)=loadcase isa StaticCase
is_modal(loadcase)=loadcase isa ModalCase
is_buckling(loadcase)=loadcase isa BucklingCase
is_time_history(loadcase)=loadcase isa TimeHistoryCase
is_response_spectrum(loadcase)=loadcase isa ResponseSpectrumCase

end


###  Some duas variáveis
# Faça um programa que some dois valores e exiba o resultado

print("inform a number ")
sum = parse(UInt8, readline())

print("inform other number ")
sum += parse(UInt8, readline())

print(sum,"\n")


# Feature Selection
# =================

"""
A list of features.

Fields
------

- `names`: feature names.
- `selected`: a bit vector of selected features.

`names` and `selected` must have the same number of elements.
"""
struct Features
    names::Vector{String}
    selected::BitVector

    function Features(names, selected)
        if length(names) != length(selected)
            throw(ArgumentError("mismatching length"))
        end
        return new(names, selected)
    end
end

"""
    Features(names)

Create a features object from feature names `names`.

All features are not selected by default. To select and unselect some features,
use `addfeatures!` and `dropfeatures!`, respectively.

`selectfeatures` algorithmically selects important features from a count matrix.
"""
Features(names::AbstractVector{String}) = Features(names, falses(length(names)))

Base.copy(features::Features) = Features(copy(features.names), copy(features.selected))

function Base.show(io::IO, features::Features)
    print(io, summary(features), "(<#features=$(nfeatures(features)),#selected=$(nselected(features))>)")
end

nfeatures(features::Features) = length(features.names)
nselected(features::Features) = sum(features.selected)
selectedfeatures(features::Features) = features.names[features.selected]

"""
    addfeatures(features, names) -> Features

Create a new features object by adding `names` to `features`.

See also `dropfeatures` and `addfeatures!`.
"""
addfeatures(features::Features, featurenames::AbstractVector{String}) = addfeatures!(copy(features), featurenames)

"""
    addfeatures!(features, names) -> Features

Add `names` to `features` in place.

See also `dropfeatures!` and `addfeatures`.
"""
function addfeatures!(features::Features, featurenames::AbstractVector{String})
    for name in featurenames
        i = findfirst(isequal(name), features.names)
        if i == nothing
            throw(ArgumentError("not found feature '$(name)'"))
        end
        features.selected[i] = true
    end
    return features
end

"""
    dropfeatures(features, names) -> Features

Create a new features object by dropping `names` from `features`.

See also `addfeatures` and `dropfeatures!`.
"""
dropfeatures(features::Features, featurenames::AbstractVector{String}) = dropfeatures!(copy(features), featurenames)

"""
    dropfeatures!(features, names) -> Features

Drop `names` from `features` in place.

See also `addfeatures!` and `dropfeatures`.
"""
function dropfeatures!(features::Features, featurenames::AbstractVector{String})
    for name in featurenames
        i = findfirst(isequal(name), features.names)
        if i == nothing
            throw(ArgumentError("not found feature '$(name)'"))
        end
        features.selected[i] = false
    end
    return features
end

"""
    selectfeatures(counts, featurenames; n_min_features=cld(size(counts, 1), 10)) -> Features

Select features from `counts`.

Arguments
---------

- `counts`: transcriptome expression matrix (features x cells).
- `featurenames`: vector of feature names.
- `n_min_features`: number of minimum features [default: 10% of all features].
"""
function selectfeatures(counts::AbstractMatrix,
                        featurenames::AbstractVector{String};
                        n_min_features::Real=cld(size(counts, 1), 10))
    M = size(counts, 1)
    if M != length(featurenames)
        throw(ArgumentError("mismatching featurenames size"))
    elseif !(1 ≤ n_min_features ≤ M)
        throw(ArgumentError("invalid n_min_features"))
    end
    mincounts, maxcounts = feature_extrema(counts)
    lowerbound = sort(maxcounts, rev=true)[n_min_features]
    return Features(featurenames, (maxcounts .≥ lowerbound) .& (maxcounts .> mincounts))
end

function feature_extrema(counts::AbstractMatrix)
    return vec(minimum(counts, dims=2)), vec(maximum(counts, dims=2))
end

# https://github.com/JuliaLang/julia/issues/27836
function feature_extrema(counts::SparseMatrixCSC{T}) where T <: Real
    m, n = size(counts)
    mincounts = fill(typemax(T), m)
    maxcounts = fill(typemin(T), m)
    for col in 1:n
        for j in counts.colptr[col]:counts.colptr[col+1]-1
            i = counts.rowval[j]
            val = counts.nzval[j]
            mincounts[i] = min(val, mincounts[i])
            maxcounts[i] = max(val, maxcounts[i])
        end
    end
    return mincounts, maxcounts
end


#####
##### `AbstractDifferential`
#####

"""
The subtypes of `AbstractDifferential` define a custom \"algebra\" for chain
rule evaluation that attempts to factor various features like complex derivative
support, broadcast fusion, zero-elision, etc. into nicely separated parts.

All subtypes of `AbstractDifferential` implement the following operations:

`+(a, b)`: linearly combine differential `a` and differential `b`

`*(a, b)`: multiply the differential `a` by the differential `b`

`Base.conj(x)`: complex conjugate of the differential `x`

`extern(x)`: convert `x` into an appropriate non-`AbstractDifferential` type for
use outside of `ChainContext`.

Valid arguments to these operations are `T` where `T<:AbstractDifferential`, or
where `T` has proper `+` and `*` implementations.

Additionally, all subtypes of `AbstractDifferential` support `Base.iterate` and
`Base.Broadcast.broadcastable(x)`.
"""
abstract type AbstractDifferential end

Base.:+(x::AbstractDifferential) = x

"""
    extern(x)

Return `x` converted to an appropriate non-`AbstractDifferential` type, for use
with external packages that might not handle `AbstractDifferential` types.

Note that this function may return an alias (not necessarily a copy) to data
wrapped by `x`, such that mutating `extern(x)` might mutate `x` itself.
"""
@inline extern(x) = x

@inline Base.conj(x::AbstractDifferential) = x

#####
##### `Wirtinger`
#####

"""
    Wirtinger(primal::Union{Number,AbstractDifferential},
              conjugate::Union{Number,AbstractDifferential})

Returns a `Wirtinger` instance representing the complex differential:

```
df = ∂f/∂z * dz + ∂f/∂z̄ * dz̄
```

where `primal` corresponds to `∂f/∂z * dz` and `conjugate` corresponds to `∂f/∂z̄ * dz̄`.

The two fields of the returned instance can be accessed generically via the
[`wirtinger_primal`](@ref) and [`wirtinger_conjugate`](@ref) methods.
"""
struct Wirtinger{P,C} <: AbstractDifferential
    primal::P
    conjugate::C
    function Wirtinger(primal::Union{Number,AbstractDifferential},
                       conjugate::Union{Number,AbstractDifferential})
        return new{typeof(primal),typeof(conjugate)}(primal, conjugate)
    end
end

wirtinger_primal(x::Wirtinger) = x.primal
wirtinger_primal(x) = x

wirtinger_conjugate(x::Wirtinger) = x.conjugate
wirtinger_conjugate(::Any) = Zero()

extern(x::Wirtinger) = throw(ArgumentError("`Wirtinger` cannot be converted to an external type."))

Base.Broadcast.broadcastable(w::Wirtinger) = Wirtinger(broadcastable(w.primal),
                                                       broadcastable(w.conjugate))

Base.iterate(x::Wirtinger) = (x, nothing)
Base.iterate(::Wirtinger, ::Any) = nothing

# TODO: define `conj` for` `Wirtinger`
Base.conj(x::Wirtinger) = throw(MethodError(conj, x))

#####
##### `Zero`
#####

"""
    Zero()
The additive identity for differentials.
This is basically the same as `0`.
"""
struct Zero <: AbstractDifferential end

extern(x::Zero) = false  # false is a strong 0. E.g. `false * NaN = 0.0`

Base.Broadcast.broadcastable(::Zero) = Ref(Zero())

Base.iterate(x::Zero) = (x, nothing)
Base.iterate(::Zero, ::Any) = nothing

#####
##### `DoesNotExist`
#####

"""
    DoesNotExist()

This differential indicates that the derivative Does Not Exist (D.N.E).
This is not the cast that it is not implemented, but rather that it mathematically
is not defined.
"""
struct DoesNotExist <: AbstractDifferential end

function extern(x::DoesNotExist)
    throw(ArgumentError("Derivative does not exit. Cannot be converted to an external type."))
end

Base.Broadcast.broadcastable(::DoesNotExist) = Ref(DoesNotExist())

Base.iterate(x::DoesNotExist) = (x, nothing)
Base.iterate(::DoesNotExist, ::Any) = nothing

#####
##### `One`
#####

"""
     One()
The Differential which is the multiplicative identity.
Basically, this represents `1`.
"""
struct One <: AbstractDifferential end

extern(x::One) = true  # true is a strong 1.

Base.Broadcast.broadcastable(::One) = Ref(One())

Base.iterate(x::One) = (x, nothing)
Base.iterate(::One, ::Any) = nothing

#####
##### `AbstractThunk
#####
abstract type AbstractThunk <: AbstractDifferential end

Base.Broadcast.broadcastable(x::AbstractThunk) = broadcastable(extern(x))

@inline function Base.iterate(x::AbstractThunk)
    externed = extern(x)
    element, state = iterate(externed)
    return element, (externed, state)
end

@inline function Base.iterate(::AbstractThunk, (externed, state))
    element, new_state = iterate(externed, state)
    return element, (externed, new_state)
end

#####
##### `Thunk`
#####

"""
    Thunk(()->v)
A thunk is a deferred computation.
It wraps a zero argument closure that when invoked returns a differential.
`@thunk(v)` is a macro that expands into `Thunk(()->v)`.

Calling a thunk, calls the wrapped closure.
`extern`ing thunks applies recursively, it also externs the differial that the closure returns.
If you do not want that, then simply call the thunk

```
julia> t = @thunk(@thunk(3))
Thunk(var"##7#9"())

julia> extern(t)
3

julia> t()
Thunk(var"##8#10"())

julia> t()()
3
```

### When to `@thunk`?
When writing `rrule`s (and to a lesser exent `frule`s), it is important to `@thunk`
appropriately.
Propagation rule's that return multiple derivatives are not able to do all the computing themselves.
 By `@thunk`ing the work required for each, they then compute only what is needed.

#### So why not thunk everything?
`@thunk` creates a closure over the expression, which (effectively) creates a `struct`
with a field for each variable used in the expression, and call overloaded.

Do not use `@thunk` if this would be equal or more work than actually evaluating the expression itself. Examples being:
- The expression wrapping something in a `struct`, such as `Adjoint(x)` or `Diagonal(x)`
- The expression being a constant
- The expression being itself a `thunk`
- The expression being from another `rrule` or `frule` (it would be `@thunk`ed if required by the defining rule already)
"""
struct Thunk{F} <: AbstractThunk
    f::F
end

"""
    @thunk expr

Define a [`Thunk`](@ref) wrapping the `expr`, to lazily defer its evaluation.
"""
macro thunk(body)
    # Basically `:(Thunk(() -> $(esc(body))))` but use the location where it is defined.
    # so we get useful stack traces if it errors.
    func = Expr(:->, Expr(:tuple), Expr(:block, __source__, body))
    return :(Thunk($(esc(func))))
end

# have to define this here after `@thunk` and `Thunk` is defined
Base.conj(x::AbstractThunk) = @thunk(conj(extern(x)))

(x::Thunk)() = x.f()
@inline extern(x::Thunk) = extern(x())

Base.show(io::IO, x::Thunk) = println(io, "Thunk($(repr(x.f)))")

"""
    InplaceableThunk(val::Thunk, add!::Function)

A wrapper for a `Thunk`, that allows it to define an inplace `add!` function,
which is used internally in `accumulate!(Δ, ::InplaceableThunk)`.

`add!` should be defined such that: `ithunk.add!(Δ) = Δ .+= ithunk.val`
but it should do this more efficently than simply doing this directly.
(Otherwise one can just use a normal `Thunk`).

Most operations on an `InplaceableThunk` treat it just like a normal `Thunk`;
and destroy its inplacability.
"""
struct InplaceableThunk{T<:Thunk, F} <: AbstractThunk
    val::T
    add!::F
end

(x::InplaceableThunk)() = x.val()
@inline extern(x::InplaceableThunk) = extern(x.val)

function Base.show(io::IO, x::InplaceableThunk)
    println(io, "InplaceableThunk($(repr(x.val)), $(repr(x.add!)))")
end

# The real reason we have this:
accumulate!(Δ, ∂::InplaceableThunk) = ∂.add!(Δ)
store!(Δ, ∂::InplaceableThunk) = ∂.add!((Δ.*=false))  # zero it, then add to it.

"""
    NO_FIELDS

Constant for the reverse-mode derivative with respect to a structure that has no fields.
The most notable use for this is for the reverse-mode derivative with respect to the
function itself, when that function is not a closure.
"""
const NO_FIELDS = DoesNotExist()

"""
    refine_differential(𝒟::Type, der)

Converts, if required, a differential object `der`
(e.g. a `Number`, `AbstractDifferential`, `Matrix`, etc.),
to another  differential that is more suited for the domain given by the type 𝒟.
Often this will behave as the identity function on `der`.
"""
function refine_differential(::Type{<:Union{<:Real, AbstractArray{<:Real}}}, w::Wirtinger)
    return wirtinger_primal(w) + wirtinger_conjugate(w)
end
refine_differential(::Any, der) = der  # most of the time leave it alone.


#!/usr/bin/env julia

using TrainRuns

train = Train("test/data/trains/freight.yaml")
path  = Path("test/data/paths/const.yaml")

runtime = trainrun(train, path)

println("The train needs $runtime seconds for the running path.")