module Pressure

using LabJack
using DataStructures
export toggle_solution, PRIMARY_SOLUTION

# LabJack digital/analog registers
const positive_cmd = "TDAC0"
const negative_cmd = "TDAC1"
const vac_supply = "EIO6"
const pressure_supply = "EIO7"
const relief_poppet = "EIO2"
const vac_delivery = "EIO1"
const pressure_delivery = "EIO0"
const valves = "EIO_STATE"
const perfusion_switch = "EIO3"
const vac_sensor = "AIN2"
const probe_sensor = "AIN3"

# We can't readout the values of TDAC, so we cache state instead
const CURRENT_PRESSURE = Threads.Atomic{Float32}(0)
const STREAM_CHANNEL = Channel{Vector{Float32}}(250)
const RUN_STREAM = Threads.Atomic{Bool}(true)
const MONITOR = Threads.Atomic{Bool}(true)
const PROBE_HISTORY = CircularBuffer{Float32}(500)
const PRIMARY_SOLUTION = Threads.Atomic{Bool}(true) # true when using "default" ACSF, false when perfusing drugs/test condition

atexit() do
    RUN_STREAM[] = false
    MONITOR[] = false
    isopen(STREAM_CHANNEL) && close(STREAM_CHANNEL)
end

function toggle_solution()
    if PRIMARY_SOLUTION[]
        write_digital(perfusion_switch, 1)
        PRIMARY_SOLUTION[] = false
    else
        write_digital(perfusion_switch, 0)
        PRIMARY_SOLUTION[] = true
    end
end

function coaxcell(amplitude, fs, seconds_per_cycle, repetitions)
    a = range(-π/2, 3π/2, length = round(Int, fs * seconds_per_cycle))
    wave = Float32.(repeat(sin.(a) .* amplitude, outer=repetitions))
    put!(STREAM_CHANNEL, wave)
end

function attempt_break(amplitude, fs, seconds_per_cycle, repetitions)
    lag = fill(Float32(-1.0), 30)
    a = collect(Float32, range(-1, amplitude, length = round(Int, fs * seconds_per_cycle)))
    a = vcat(lag, a, lag)
    wave = repeat(a, outer=repetitions)
    put!(STREAM_CHANNEL, wave)
end

function get_pressure()
    return CURRENT_PRESSURE[]
end

function init_levels()
    # initialize to safe values
    write_digital(perfusion_switch, 0)
    write_digital(pressure_supply, 0)
    write_digital(vac_delivery, 0)
    write_digital(pressure_delivery, 0)
    write_analog(positive_cmd, 0.0)
    write_analog(negative_cmd, 0.0)
end

function pressure_transduce(volts)::Float32
    return (volts - 3.0502)*50.0
end

function vac_supply_transduce(volts)::Float32
    return (1 - (volts - 1) / 4) * -101
end

function vac_monitor()
    @async while MONITOR[]
        v = read_analog(vac_sensor)
        p = vac_supply_transduce(v)
        if p >= -60
            write_digital(vac_supply, 1)
            sleep(5.0)
            write_digital(vac_supply, 0)
        else
            yield()
            sleep(30.0)
        end
        GC.safepoint()
    end
end

function probe_monitor()
    @async while MONITOR[]
        v = read_analog(probe_sensor)
        p = pressure_transduce(v)
        push!(PROBE_HISTORY, p)
        GC.safepoint()
        yield()
        sleep(0.02)
    end
end

function initialize()
    LabJack.init_default()
    init_levels()
    vac_monitor()
    fill!(PROBE_HISTORY, Float32(0.0))
    probe_monitor()
end

function stream_out(cycle_lock::Threads.Condition)
    @async while RUN_STREAM[]
        pressures = take!(STREAM_CHANNEL)
        for val in pressures
            lock(cycle_lock)
            try
                wait(cycle_lock)
                set(val)
                GC.safepoint()
            finally
                unlock(cycle_lock)
            end
        end
    end
end

function set(value)
    if value !== CURRENT_PRESSURE[]
        Threads.atomic_xchg!(CURRENT_PRESSURE, value)
        if value == 0
            write_analog(positive_cmd, 0.0)
            write_analog(negative_cmd, 0.0)
            write_digital(relief_poppet, 1)
            write_digital(vac_delivery, 1)
            write_digital(pressure_delivery, 1)
        elseif value > 0
            write_digital(vac_delivery, 0)
            write_digital(pressure_delivery, 1)
            write_analog(positive_cmd, 10*(value/101))
            write_digital(relief_poppet, 0)
        elseif value < 0
            write_digital(vac_delivery, 1)
            write_digital(pressure_delivery, 0)
            write_analog(negative_cmd, 10*(value/-101))
            write_digital(relief_poppet, 0)
        end
    end
end

end


function schedule(employees, env)
    # Wrap the scheduling in windowing!
    start_index, end_index, time_delta = get_window(env["coverage"])

    # Apply the window
    env["time_between_coverage"] += time_delta
    if "cycle_length" in keys(env)
        env["cycle_length"] -= time_delta
    end
    if "no_shifts_after" in keys(env)
        env["no_shifts_after"] -= time_delta
    end
    env["coverage"] = apply_window(env["coverage"], start_index, end_index)
    # todo - do we have a "time since last shift" at start of week?
    for e in keys(employees)
        employees[e]["availability"] = apply_window(employees[e]["availability"], start_index, end_index)
    end
    Logging.info( "Windowing removed $time_delta hours per day (start index $start_index / end index $end_index)")

    ok, shifts = run_schedule(employees, env)
    if !ok
        Logging.info( "Scheduling not ok at windower")
        return ok, {}
    end

    # Exit the window and return
    return ok, remove_window(shifts, start_index)
end

function run_schedule(employees, env)
    if length(employees) == 0
        Logging.info( "Switching to unassigned shifts mode")
        ok, shifts = run_schedule_unassigned(env)
    else
        ok, shifts = run_schedule_standard(employees, env)
    end
    return ok, shifts
end

function run_schedule_standard(employees, env)
    attempt = 1
    employees = meet_base_coverage(env, employees)

    @label schedule_start

    ok, employees, env = build_week(employees, env)
    if !ok
        Logging.info( "Build failed")
        return false, {}
    end

    ok, message = validate_input(employees, env)
    if !ok
        Logging.info( message)
        return false, {}
    end

    # Check days per week is large enough for consec
    if size(env["coverage"], 1) > 2
        # Schedule consec first, and do non-consec as a last resort
        consecutive = true
        ok, weekly_schedule_consecutive = schedule_week(employees, env, consecutive)
        if ok
            Logging.info( "Consecutive schedule passed.")
            return true, weekly_schedule_consecutive
        end
        Logging.info( "Consecutive schedule validation failed. Trying inconsecutive.")
    end

    consecutive = false
    ok, weekly_schedule = schedule_week(employees, env, consecutive)
    if ok
        return true, weekly_schedule
    end

    # Otherwise - make an unassigned shift and restart
    name, val = generate_unassigned_shift(env)
    Logging.info( "Unable to meet coverage. Adding an unassigned shift and repeating. (end of attempt $attempt)")
    employees[name] = val
    attempt += 1
    @goto schedule_start
end

function run_schedule_unassigned(env)
    total_hours = sum(sum(env["coverage"]))
    Logging.info( "Coverage total hours: $total_hours")
    if total_hours > BIFURCATE_THRESHOLD
        # Generate subproblems
        Logging.info( "Split env into sub problems")
        ok, shifts = bifurcate_unassigned_scheduler(env)
        if !ok
            Logging.err("Bifurcatin failed in unassigned shift")
        end
        return ok, shifts
    end

    # Schedule day by day
    week_length = size(env["coverage"], 1)
    week_employees = Dict()
    shifts = Dict()
    for day in 1:week_length
        Logging.info( "Starting day scheduler day $day of $week_length")
        # Build a day env
        day_env = deepcopy(env)
        day_env["coverage"] = env["coverage"][day]

        # Generate lots of shifts
        day_employees = meet_unassigned_base_coverage(env, day)

        ok = false
        attempt = 1
        # Define day_shifts up here due to scoping
        day_shifts = Dict()
        while !ok
            # Change week employees to day employees
            day_employees_filtered = Dict()
            for e in keys(day_employees)
                d = deepcopy(day_employees[e])
                d["availability"] = day_employees[e]["availability"][day]
                d["min"] = d["shift_time_min"]
                d["max"] = d["shift_time_max"]
                day_employees_filtered[e] = d
            end

            # Run day schedule
            ok, day_shifts = schedule_day(day_employees_filtered, day_env)

            if !ok
                Logging.info( "Unable to meet coverage. Adding an unassigned shift and repeating. (end of attempt $attempt on day $day)")
                name, val = generate_unassigned_shift(env)
                day_employees[name] = val
                attempt += 1
            end
        end

        # push for testing later
        for k in keys(day_employees)
            week_employees[k] = day_employees[k]
        end

        # Now we  have to do post-processing to adjust the day correctly
        for name in keys(day_shifts)
            shift =  day_shifts[name]
            # Being really careful
            shift["day"] = day
            if !(name in keys(shifts))
                shifts[name] = Any[]
            end
            push!(shifts[name], shift)
        end
    end

    ok, message= validate_schedule(week_employees, env, shifts)
    if !ok
        Logging.err("Unassigned shift schedule invalid after post-processing - $message")
    end
    return ok, shifts
end

function schedule_week(employees, env, consecutive)

    # Setup
    valid_schedules = Any[]
    prior_lift = false
    lift_ceiling = false
    weekly_schedule = Dict{Any,Any}
    start_day = 0
    start_time = time()
    perfect_optimality = week_sum_coverage(env)

    # Start the loop, captain!
    while (prior_lift == false || (prior_lift * LYFT_THROTTLE > MIN_LYFT))
        if (time() > start_time + ITERATION_TIMEOUT) && (length(valid_schedules) == 0)
            Logging.info("Iteration timeout hit")
            break
        end

        if (time() > start_time + SEARCH_TIMEOUT) && length(valid_schedules) > 0
            Logging.info("Search timeout hit")
            break
        end

        if prior_lift != false
            lift_ceiling = prior_lift * LYFT_THROTTLE
        end

        ok, employees, prior_lift = assign_employees_to_days(employees, env, consecutive, lift_ceiling)
        if !ok
            Logging.info( "consecutive $consecutive day assignment failed")
            break
        end

        # Set up parallel calculations
        calculations = Any[]

        # Only run one scheduler if it's a single day.
        if size(env["coverage"], 1) > 1
            methods = ["balanced", "greedy"]
        else
            methods = ["balanced"]
        end

        for start_day=1:size(env["coverage"], 1)
            for method in methods
                push!(calculations, {
                    "start_day" => start_day,
                    "method" => method,
                    "employees" => employees,
                    "env" => env,
                })
            end
        end

        results = pmap(schedule_by_day, calculations)
        lift_schedules = map(return_schedules, filter(valid_schedules_net, results))
        num = length(lift_schedules)
        Logging.info( "Found $num schedules this lift")
        for valid_schedule=lift_schedules
            hours = week_hours_scheduled(valid_schedule)
            if hours == perfect_optimality
                return true, valid_schedule
            end
            push!(valid_schedules, valid_schedule)
        end
    end

    if length(valid_schedules) == 0
        return false, {}
    end

    best_hours = 0
    best_schedule = None
    for test_schedule=valid_schedules
        test_hours = week_hours_scheduled(test_schedule)
        if (test_hours < best_hours) || (best_schedule == None)
            best_hours = test_hours
            best_schedule = test_schedule
        end
    end

    if best_schedule == None
        num = length(valid_schedules)
        Logging.info( "Found $num schedules")
        return false, {}
    end
    elapsed = (time() - start_time) / 60
    num_schedules = length(valid_schedules)
    hours = week_hours_scheduled(best_schedule)
    overage = (hours - perfect_optimality) / perfect_optimality
    Logging.info( "Full time took $elapsed minutes and ended at lift $prior_lift. Found $num_schedules valid schedules. Best overage $overage")
    return true, best_schedule

end

function build_week(employees, env)
    # First check for some required shit in the environment
    if !("shift_time_min" in keys(env))
        # turn employees into a message variable
        return false, "shift_time_min not in environment", {}
    end

    if !("shift_time_max" in keys(env))
        Logging.info( "shift_time_max not in environment")
        return false, {}, {}
    end

    days_per_week = size(env["coverage"], 1)
    # Pick number of shifts per employee
    for e in keys(employees)

        # This has to be run now due to later function dependencies
        if (size(employees[e]["availability"], 1) != days_per_week)
            Logging.info( string("Employee ", e, " availability wrong number days per week"))
            return false, {}, {}
        end

        # set employee to unavailable when coverage is 0
        for day in 1:days_per_week
            for t in size(env["coverage"][day])
                if env["coverage"][day][t] == 0
                    employees[e]["availability"][day][t] = 0
                end
            end
        end

        # shift time
        if !("shift_time_min" in keys(employees[e]))
            # inherit from env if not defined
            employees[e]["shift_time_min"] = env["shift_time_min"]
        end

        if !("shift_time_max" in keys(employees[e]))
            employees[e]["shift_time_max"] = env["shift_time_max"]
        end

        if (
            "shift_count_min" in keys(employees[e])
            && "shift_count_max" in keys(employees[e])
        )
            continue
        end

        # calculate based on min/max
        if "shift_count" in keys(employees[e])
            employees[e]["shift_count_min"] = employees[e]["shift_count"]
            employees[e]["shift_count_max"] = employees[e]["shift_count"]
            continue
        end

        if !("shift_count_min" in keys(employees[e]))
            # calculate from limits
            employees[e]["shift_count_min"] = int(ceil(employees[e]["hours_min"] / employees[e]["shift_time_max"]))
        end

        if !("shift_count_max" in keys(employees[e]))
            max = int(floor(employees[e]["hours_max"] / employees[e]["shift_time_min"]))
            # can't exceed days per week
            if max > days_per_week
                max = days_per_week
            end
            employees[e]["shift_count_max"] = max
        end

        # Set ceiling of shift_count_max at availability
        days_available = days_available_per_week(employees[e])
        if employees[e]["shift_count_max"] > days_available
            employees[e]["shift_count_max"] = days_available
        end

    end

    # Anneal availability - get rid of availability that isn't feasible
    for e in keys(employees)
        employees[e]["availability"] = anneal_availability(employees[e]["availability"], employees[e]["shift_time_min"])
    end

    # day availability - num hours using longest_shift
    for e in keys(employees)
        longest_per_day = Int[]
        for day in 1:days_per_week
            push!(longest_per_day, get_longest_availability(employees[e], day))
        end
        employees[e]["longest_availability"] = longest_per_day
    end

    return true, employees, env
end

function validate_input(employees, env)
    # first build an array that is the length of each day of the week
    day_length = Int[]
    days_per_week = size(env["coverage"], 1)

    for day in 1:days_per_week
        push!(day_length, length(env["coverage"][day]))
    end

    # Days must be the same length
    overage = 0
    for day in 2:days_per_week
        if day_length[day] != day_length[1]
            return false, "ERROR: excessive day length mismatch"
        end
    end

    # Build the coverage count
    coverage_count_per_hour = Any[]
    for day in 1:size(day_length, 1)
        push!(coverage_count_per_hour, zeros(Int, day_length[day]))
    end

    # sum this from employee longest_availability variable
    coverage_count_per_day = zeros(Int, days_per_week)

    # Loop through employees
    # do shit, including adding to coverage
    for e in keys(employees)
        for t in 1:days_per_week
            if size(employees[e]["availability"][t], 1) != day_length[t]
                return false, string("Employee ", e, " availability for day ", t, " wrong number of hours")
            end
            # 1) Push hourly availability to per-hour coverage
            for h in day_length[t]
                coverage_count_per_hour[t][h] += employees[e]["availability"][t][h]
            end
            # Check their max availability per day
            coverage_count_per_day[t] += employees[e]["longest_availability"][t]
        end
        # Make copy of longest_availability then sort it; largest first
        avail = sort(employees[e]["longest_availability"], rev=true)
        # check that the sum of largest "longest avail" >= hours_min
        avail_sum_max = 0
        for i in 1:employees[e]["shift_count_max"]
            # bounds check - threw an error before
            if i <= length(avail)
                avail_sum_max += avail[i]
            end
        end

        if avail_sum_max < employees[e]["hours_min"]
            # check that sum of longest_availability > hours_min
            return false, string("Employee ", e, " not available for min_hours")
        end
    end

    for e in keys(employees)
        # Shift count validations
        shift_count_min = employees[e]["shift_count_min"]
        shift_count_max = employees[e]["shift_count_max"]
        shift_time_min = employees[e]["shift_time_min"] # inherited from env
        shift_time_max = employees[e]["shift_time_max"]
        hours_min = employees[e]["hours_min"]
        hours_max = employees[e]["hours_max"]
        days_available = days_available_per_week(employees[e])

        if shift_count_min > days_available
            return false, "Employee $e has fewer days available than minimum shift count"
        end

        if shift_count_max > days_per_week
            return false, "shift_count_max exceeds days per week"
        end

        if shift_count_min > shift_count_max
            return false, "shift count min is larger than shift count max!"
        end

        if shift_count_min * shift_time_max < hours_min
            return false, "Shift count min is too low for employee $e"
        end

        if shift_count_max * shift_time_min > hours_max
            return false, "Shift count max is too high for employee $e"
        end
    end

    return true, ""
end

function assign_employees_to_days(employees, env, consecutive=false, lift_ceiling=false)
    #=
    Consecutive days off works by looking at days_per_week, and if an employee
    is scheduled for <= (days_per_week-2) shifts, then at least two of their
    days off must be consecutive.
    =#
    days_per_week = size(env["coverage"], 1)
    number_of_employees = length(employees)
    decision_index_max = days_per_week * number_of_employees
    day_length = Int[]

    for day in 1:days_per_week
        push!(day_length, length(env["coverage"][day]))
    end

    if GUROBI
        m = Model(
            solver=GurobiSolver(
                LogToConsole=0,
                TimeLimit=CALCULATION_TIMEOUT,
                OutputFlag=0,
            )
        )
    else
        m = Model(
            solver=CbcSolver(
                threads=Base.CPU_CORES,
            )
        )
    end

    # Decision Variables
    @defVar(m, decision_variable[1:decision_index_max], Bin)

    if consecutive
        @defVar(m, decision_consecutive[1:decision_index_max], Bin)
    end

    #=
    Lyft tries to evenly allocate extra resources between days. This way,
    if you have excess capacity, it is spread evenly over days and thus
    maximizes likelihood of feasibility.
    Ignoring lift may mean that one day gets all the excess capacity. This sucks.
    =#
    @defVar(m, lift >= 1)

    decision_index_to_employee = String[]
    decision_index_to_day = Int[]
    decision_index_hours = Int[]
    for e in keys(employees)
        for t in 1:days_per_week
            push!(decision_index_to_employee, e)
            push!(decision_index_to_day, t)
            push!(decision_index_hours, employees[e]["longest_availability"][t])
        end
    end

    # Now we do per-employee constraint
    # Note: decision var 1st e.g. use x*3 instead of 3*x
    for e in keys(employees)
        # days assigned = # shifts
        # Break into to sections due to Gurobi range issue in Julia
        @addConstraint(m,
            employees[e]["shift_count_min"] <= sum{decision_variable[i], i=1:decision_index_max; decision_index_to_employee[i] == e}
        )

        @addConstraint(m,
            sum{decision_variable[i], i=1:decision_index_max; decision_index_to_employee[i] == e} <= employees[e]["shift_count_max"]
        )

        # assigned days * longest availability > min week hours
        @addConstraint(m,
            sum{decision_index_hours[i] * decision_variable[i], i=1:decision_index_max; decision_index_to_employee[i] == e} >= employees[e]["hours_min"]
        )

        if consecutive
            if employees[e]["shift_count_max"] < (days_per_week - 1)
                @addConstraint(m,
                    sum{decision_consecutive[i], i=1:decision_index_max; decision_index_to_employee[i] == e } >= 1
                )
            end
        end
    end

    for i in 1:decision_index_max
        # If an employee has no availability - don't schedule them!
        if decision_index_hours[i] == 0
            @addConstraint(m, decision_variable[i] == 0)
        end

        if consecutive
            if decision_index_to_day[i] == 1
                # See if employee worked day before this week
                if ("worked_day_preceding_week" in keys(employees[decision_index_to_employee[i]])) && (employees[decision_index_to_employee[i]]["worked_day_preceding_week"] == false)
                    @addConstraint(m, decision_consecutive[i] + decision_variable[i] == 1)
                else
                    # throw away the first index - it defaults to zero
                    @addConstraint(m, decision_consecutive[i] == 0)
                end
            else # day > 1
                # Goal: decision_consecutive[i] = 1 IF AND ONLY IF
                # decision_variable[i]=0 and decision_variable[i-1]=0
                @addConstraint(m, (decision_variable[i] + decision_variable[i-1]) >= 1 - decision_consecutive[i])
                @addConstraint(m, (2 - decision_variable[i] - decision_variable[i-1]) >= 2*decision_consecutive[i])
            end

        end
    end

    for d in 1:days_per_week
        # sum of day coverage is sufficient based on longest avail.
        @addConstraint(m,
            sum{decision_index_hours[i] * decision_variable[i], i=1:decision_index_max; decision_index_to_day[i] == d} >= sum(env["coverage"][d])*lift
        )

        # ensure hourly coverage
        for t in 1:day_length[d]
            @addConstraint(m,
                sum{employees[decision_index_to_employee[i]]["availability"][d][t] * decision_variable[i], i=1:decision_index_max; decision_index_to_day[i] == d} >= env["coverage"][d][t]
            )
        end
    end

    # For looping if infeasible models
    if lift_ceiling != false
        @addConstraint(m, lift <= lift_ceiling)
    end

    # Objective is to maximize that people get assigned to days
    # when they are free
    @setObjective(m, Max, lift)

    status = solve(m)
    if status == :Optimal
        i = 0
        for e in keys(employees)
            assigned = Int[]
            for t in 1:days_per_week
                i += 1
                push!(assigned, round(getValue(decision_variable[i])))
            end
            employees[e]["days_assigned"] = assigned
        end
        lift_value = getValue(lift)
        if days_per_week == 1
            # override. Weird bug, but we can have different lifts per day??
            lift_value = 1
        end
        Logging.info( "Lyft: ", lift_value)
        gc()
        return true, employees, lift_value
    else
        Logging.info( "Infeasible")
        gc()
        return false, {}, 0
    end
    # ok
    gc()
    return true, schedule
end

function validate_schedule(employees, env, weekly_schedule)
    days_per_week = size(env["coverage"], 1)

    day_length = Int[]
    days_per_week = size(env["coverage"], 1)

    for day in 1:days_per_week
        push!(day_length, length(env["coverage"][day]))
    end

    sum_hours = 0

    sum_coverage_by_hour = Any[]
    for day in 1:days_per_week
        push!(sum_coverage_by_hour, zeros(Int, day_length[day]))
    end

    for e in keys(weekly_schedule)
        num_shifts = length(weekly_schedule[e])

        # employees have shift_count within bounds
        if num_shifts < employees[e]["shift_count_min"]
            return false, "Employee $e has too few shifts"
        end

        if num_shifts > employees[e]["shift_count_max"]
            return false, "Employee $e has too many shifts"
        end

        employee_sum_hours = 0

        last_shift = null
        for i in 1:num_shifts
            shift_day = weekly_schedule[e][i]["day"]
            shift_start = weekly_schedule[e][i]["start"]
            shift_length = weekly_schedule[e][i]["length"]

            # Sum hours
            sum_hours += shift_length
            employee_sum_hours += shift_length

            # Coverage
            for t in shift_start:(shift_start + shift_length - 1)
                sum_coverage_by_hour[shift_day][t] += 1
            end

            # employees have shift lengths within bounds
            if shift_length < employees[e]["shift_time_min"]
                return false, "Employee $e shift $i length too short on Day $shift_day - $shift_length"
            end

            if shift_length > employees[e]["shift_time_max"]
                return false, "Employee $e shift $i length too long on Day $shift_day - $shift_length"
            end

            # INTERSHIFT
            if shift_day > 1 # check for intershift with last shift
                # find shift for preceding day - may not be in order
                for j in 1:num_shifts
                    # Intershift is not supposed to cover more than one day
                    last_shift_day = shift_day - 1
                    if weekly_schedule[e][j]["day"] == last_shift_day
                        last_shift_start = weekly_schedule[e][j]["start"]
                        last_shift_length = weekly_schedule[e][j]["length"]
                        last_shift_end = last_shift_start + last_shift_length - 1
                        hours_left_in_day = length(env["coverage"][last_shift_day]) - last_shift_end

                        if "intershift" in keys(employees[e])
                            intershift = employees[e]["intershift"]
                        else
                            intershift = env["intershift"]
                        end

                        overflow = intershift - hours_left_in_day - env["time_between_coverage"]
                        if overflow > shift_start
                            return false, "Not enough time before day $shift_day (overflow $overflow / shift start $shift_start)"
                        end
                    end
                end
            end
        end

        # employees have total hours within range
        if employee_sum_hours < employees[e]["hours_min"]
            return false, "Employee $e works too few hours in the week"
        end

        if employee_sum_hours > employees[e]["hours_max"]
            return false, "Employee $e works too many hours in the week"
        end

    end
    sum_coverage = sum(sum(env["coverage"]))

    # Hour by hour coverage
    for day in 1:days_per_week
        for hour in 1:day_length[day]
            if env["coverage"][day][hour] > sum_coverage_by_hour[day][hour]
                return false, "Insufficient employees working on day $day at hour $hour"
            end
        end
    end

    return true, "balling"
end

function valid_schedules_net(schedule)
    # For use with schedule_by_day in parallel mode
    return schedule["success"]
end

function return_schedules(schedule)
    # For use with schedule_by_day in parallel mode
    return schedule["schedule"]
end


# This file is a part of Julia. License is MIT: https://julialang.org/license

## CHOLMOD
const TRUE  = Int32(1)
const FALSE = Int32(0)

## itype defines the types of integer used:
const INT     = Int32(0)  # all integer arrays are int
const INTLONG = Int32(1)  # most are int, some are SuiteSparse_long
const LONG    = Int32(2)  # all integer arrays are SuiteSparse_long

## dtype defines what the numerical type is (double or float):
const DOUBLE = Int32(0)        # all numerical values are double
const SINGLE = Int32(1)        # all numerical values are float
dtyp(::Type{Float32}) = SINGLE
dtyp(::Type{Float64}) = DOUBLE
dtyp(::Type{ComplexF32}) = SINGLE
dtyp(::Type{ComplexF64}) = DOUBLE

## xtype defines the kind of numerical values used:
const PATTERN = Int32(0)       # pattern only, no numerical values
const REAL    = Int32(1)       # a real matrix
const COMPLEX = Int32(2)       # a complex matrix (ANSI C99 compatible)
const ZOMPLEX = Int32(3)       # a complex matrix (MATLAB compatible)
xtyp(::Type{Float32})    = REAL
xtyp(::Type{Float64})    = REAL
xtyp(::Type{ComplexF32}) = COMPLEX
xtyp(::Type{ComplexF64}) = COMPLEX

## Scaling modes, selected by the scale input parameter:
const SCALAR = Int32(0)        # A = s*A
const ROW    = Int32(1)        # A = diag(s)*A
const COL    = Int32(2)        # A = A*diag(s)
const SYM    = Int32(3)        # A = diag(s)*A*diag(s)

## Types of systems to solve
const CHOLMOD_A    = Int32(0)          # solve Ax=b
const CHOLMOD_LDLt = Int32(1)          # solve LDL'x=b
const CHOLMOD_LD   = Int32(2)          # solve LDx=b
const CHOLMOD_DLt  = Int32(3)          # solve DL'x=b
const CHOLMOD_L    = Int32(4)          # solve Lx=b
const CHOLMOD_Lt   = Int32(5)          # solve L'x=b
const CHOLMOD_D    = Int32(6)          # solve Dx=b
const CHOLMOD_P    = Int32(7)          # permute x=Px
const CHOLMOD_Pt   = Int32(8)          # permute x=P'x

# Symmetry types
const EMPTY                 =-1
const MM_RECTANGULAR        = 1
const MM_UNSYMMETRIC        = 2
const MM_SYMMETRIC          = 3
const MM_HERMITIAN          = 4
const MM_SKEW_SYMMETRIC     = 5
const MM_SYMMETRIC_POSDIAG  = 6
const MM_HERMITIAN_POSDIAG  = 7

# check the size of SuiteSparse_long
if Int(ccall((:jl_cholmod_sizeof_long, :libsuitesparse_wrapper),Csize_t,())) == 4
    const SuiteSparse_long = Int32
    const IndexTypes = (:Int32,)
    const ITypes = Union{Int32}
else
    const SuiteSparse_long = Int64
    const IndexTypes = (:Int32, :Int64)
    const ITypes = Union{Int32, Int64}
end
ityp(::Type{SuiteSparse_long}) = LONG

const VTypes = Union{ComplexF64, Float64}
const VRealTypes = Union{Float64}

struct CHOLMODException <: Exception
    msg::String
end

macro isok(A)
    :($(esc(A)) == TRUE || throw(CHOLMODException("")))
end


struct AddCostSpec
    variable_type::Type
    component_type::Type
    has_status_variable::Bool
    has_status_parameter::Bool
    sos_status::SOS_STATUS_VARIABLE
    multiplier::Float64
    variable_cost::Union{Nothing, Function}
    start_up_cost::Union{Nothing, Function}
    shut_down_cost::Union{Nothing, Function}
    fixed_cost::Union{Nothing, Function}
    addtional_linear_terms::Dict{String, Symbol}
end

function AddCostSpec(;
    variable_type,
    component_type,
    has_status_variable = false,
    has_status_parameter = false,
    sos_status = NO_VARIABLE,
    multiplier = OBJECTIVE_FUNCTION_POSITIVE,
    variable_cost = nothing,
    start_up_cost = nothing,
    shut_down_cost = nothing,
    fixed_cost = nothing,
    addtional_linear_terms = Dict{String, Symbol}(),
)
    return AddCostSpec(
        variable_type,
        component_type,
        has_status_variable,
        has_status_parameter,
        sos_status,
        multiplier,
        variable_cost,
        start_up_cost,
        shut_down_cost,
        fixed_cost,
        addtional_linear_terms,
    )
end

function AddCostSpec(
    ::Type{<:T},
    ::Type{<:U},
    ::PSIContainer,
) where {T <: PSY.Component, U <: AbstractDeviceFormulation}
    error("AddCostSpec is not implemented for $T / $U")
end

set_addtional_linear_terms!(spec::AddCostSpec, key, value) =
    spec.addtional_linear_terms[key] = value

function add_service_variables!(spec::AddCostSpec, services)
    for service in services
        name = PSY.get_name(service)
        set_addtional_linear_terms!(spec, name, make_variable_name(name, typeof(service)))
    end
    return
end

"""
Add variables to the PSIContainer for a service.
"""
function cost_function!(
    psi_container::PSIContainer,
    devices::IS.FlattenIteratorWrapper{T},
    ::DeviceModel{T, U},
    ::Type{<:PM.AbstractPowerModel},
    feedforward::Union{Nothing, AbstractAffectFeedForward} = nothing,
) where {T <: PSY.Component, U <: AbstractDeviceFormulation}
    for d in devices
        spec = AddCostSpec(T, U, psi_container)
        @debug T, spec
        services = PSY.get_services(d)
        add_service_variables!(spec, services)
        add_to_cost!(psi_container, spec, PSY.get_operation_cost(d), d)
    end
    return
end

function add_to_cost_expression!(
    psi_container::PSIContainer,
    cost_expression::JuMP.AbstractJuMPScalar,
)
    T_ce = typeof(cost_expression)
    T_cf = typeof(psi_container.cost_function)
    if T_cf <: JuMP.GenericAffExpr && T_ce <: JuMP.GenericQuadExpr
        psi_container.cost_function += cost_expression
    else
        JuMP.add_to_expression!(psi_container.cost_function, cost_expression)
    end
    return
end

function has_on_variable(
    psi_container::PSIContainer,
    ::Type{T};
    variable_type = OnVariable,
) where {T <: PSY.Component}
    #get_variable can't be used because the default behavior is to error if variables is not present
    return !isnothing(get(
        psi_container.variables,
        make_variable_name(variable_type, T),
        nothing,
    ))
end

function has_on_parameter(psi_container::PSIContainer, ::Type{T}) where {T <: PSY.Component}
    if !model_has_parameters(psi_container)
        return false
    end
    return !isnothing(get(
        psi_container.parameters,
        encode_symbol(OnVariable, string(T)),
        nothing,
    ))
end

function _get_pwl_vars_container(psi_container::PSIContainer)
    if !haskey(psi_container.variables, :PWL_cost_vars)
        time_steps = model_time_steps(psi_container)
        contents = Dict{Tuple{String, Int, Int}, Any}()
        container = JuMP.Containers.SparseAxisArray(contents)
        assign_variable!(psi_container, :PWL_cost_vars, container)
    else
        container = get_variable(psi_container, :PWL_cost_vars)
    end
    return container
end

function slope_convexity_check(slopes::Vector{Float64})
    flag = true
    for ix in 1:(length(slopes) - 1)
        if slopes[ix] > slopes[ix + 1]
            @debug slopes
            return flag = false
        end
    end
    return flag
end

@doc raw"""
Returns True/False depending on compatibility of the cost data with the linear implementation method

Returns ```flag```

# Arguments

* cost_ : container for quadratic and linear factors
"""
function pwlparamcheck(cost_)
    slopes = PSY.get_slopes(cost_)
    # First element of the return is the average cost at P_min.
    # Shouldn't be passed for convexity check
    return slope_convexity_check(slopes[2:end])
end

function linear_gen_cost!(
    psi_container::PSIContainer,
    var_name::Symbol,
    component_name::String,
    linear_term::Float64,
    time_period::Int,
)
    resolution = model_resolution(psi_container)
    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    variable = get_variable(psi_container, var_name)[component_name, time_period]
    gen_cost = sum(variable) * linear_term
    add_to_cost_expression!(psi_container, gen_cost * dt)
    return
end

@doc raw"""
Returns piecewise cost expression using SOS Type-2 implementation for psi_container model.

# Equations

``` variable = sum(sos_var[i]*cost_data[2][i])```

``` gen_cost = sum(sos_var[i]*cost_data[1][i]) ```

# LaTeX

`` variable = (sum_{i\in I} c_{2, i} sos_i) ``

`` gen_cost = (sum_{i\in I} c_{1, i} sos_i) ``

Returns ```gen_cost```

# Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* variable::JuMP.Containers.DenseAxisArray{JV} : variable array
* cost_data::PSY.VariableCost{NTuple{2, Float64}} : container for quadratic and linear factors
"""
function pwl_gencost_sos!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component_name::String,
    cost_data::Vector{NTuple{2, Float64}},
    time_period::Int,
)
    var_name = make_variable_name(spec.variable_type, spec.component_type)
    variable = get_variable(psi_container, var_name)[component_name, time_period]
    settings_ext = get_ext(get_settings(psi_container))
    export_pwl_vars = get_export_pwl_vars(psi_container.settings)
    @debug export_pwl_vars
    total_gen_cost = JuMP.AffExpr(0.0)

    if spec.sos_status == NO_VARIABLE
        bin = 1.0
        @debug("Using Piecewise Linear cost function but no variable/parameter ref for ON status is passed. Default status will be set to online (1.0)")
    elseif spec.sos_status == PARAMETER
        param_key = encode_symbol(OnVariable, string(spec.component_type))
        bin =
            get_parameter_container(psi_container, param_key).parameter_array[component_name]
        @debug("Using Piecewise Linear cost function with parameter $(param_key)")
    elseif spec.sos_status == VARIABLE
        var_key = make_variable_name(OnVariable, spec.component_type)
        bin = get_variable(psi_container, var_key)[component_name, time_period]
        @debug("Using Piecewise Linear cost function with variable $(var_key)")
    else
        @assert false
    end

    gen_cost = JuMP.AffExpr(0.0)
    pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data))
    for i in 1:length(cost_data)
        pwlvars[i] = JuMP.@variable(
            psi_container.JuMPmodel,
            base_name = "{$(variable)}_{sos}",
            start = 0.0,
            lower_bound = 0.0,
            upper_bound = 1.0
        )
        if export_pwl_vars
            container = _get_pwl_vars_container(psi_container)
            container[(component_name, time_period, i)] = pwlvars[i]
        end
        JuMP.add_to_expression!(gen_cost, cost_data[i][1] * pwlvars[i])
    end
    JuMP.@constraint(psi_container.JuMPmodel, sum(pwlvars) == bin)
    JuMP.@constraint(
        psi_container.JuMPmodel,
        pwlvars in MOI.SOS2(collect(1:length(pwlvars)))
    )
    JuMP.@constraint(
        psi_container.JuMPmodel,
        variable == sum([var_ * cost_data[ix][2] for (ix, var_) in enumerate(pwlvars)])
    )
    JuMP.add_to_expression!(total_gen_cost, gen_cost)

    return total_gen_cost
end

@doc raw"""
Returns piecewise cost expression using linear implementation for psi_container model.

# Equations

``` 0 <= pwl_var[i] <= (cost_data[2][i] - cost_data[2][i-1])```

``` variable = sum(pwl_var[i])```

``` gen_cost = sum(pwl_var[i]*cost_data[1][i]/cost_data[2][i]) ```

# LaTeX
`` 0 <= pwl_i <= (c_{2, i} - c_{2, i-1})``

`` variable = (sum_{i\in I} pwl_i) ``

`` gen_cost = (sum_{i\in I}  pwl_i) c_{1, i}/c_{2, i} ``

Returns ```gen_cost```

# Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* variable::JuMP.Containers.DenseAxisArray{JV} : variable array
* cost_data::Vector{NTuple{2, Float64}} : container for quadratic and linear factors
"""
function pwl_gencost_linear!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component_name::String,
    cost_data::Vector{NTuple{2, Float64}},
    time_period::Int,
)
    var_name = make_variable_name(spec.variable_type, spec.component_type)
    variable = get_variable(psi_container, var_name)[component_name, time_period]
    settings_ext = get_ext(get_settings(psi_container))
    export_pwl_vars = get_export_pwl_vars(psi_container.settings)
    @debug export_pwl_vars
    total_gen_cost = JuMP.AffExpr(0.0)

    gen_cost = JuMP.AffExpr(0.0)
    total_gen = JuMP.AffExpr(0.0)
    for i in 1:length(cost_data)
        pwlvar = JuMP.@variable(
            psi_container.JuMPmodel,
            base_name = "{$(variable)}_{pwl}",
            start = 0.0,
            lower_bound = 0.0,
            upper_bound = PSY.get_breakpoint_upperbounds(cost_data)[i]
        )
        if export_pwl_vars
            container = _get_pwl_vars_container(psi_container)
            container[(component_name, time_period, i)] = pwlvar
        end
        JuMP.add_to_expression!(gen_cost, PSY.get_slopes(cost_data)[i] * pwlvar)
        JuMP.add_to_expression!(total_gen, pwlvar)
    end
    JuMP.@constraint(psi_container.JuMPmodel, variable == total_gen)
    JuMP.add_to_expression!(total_gen_cost, gen_cost)
    return total_gen_cost
end

"""
Adds to the models costs represented by PowerSystems TwoPart costs
"""
function add_to_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    cost_data::PSY.TwoPartCost,
    component::PSY.Component,
)
    component_name = PSY.get_name(component)
    time_steps = model_time_steps(psi_container)
    @debug "TwoPartCost" component_name
    if !(spec.variable_cost === nothing)
        variable_cost = spec.variable_cost(cost_data)
        for t in time_steps
            variable_cost!(psi_container, spec, component_name, variable_cost, t)
        end
    else
        @warn "No variable cost defined for $component_name"
    end

    if !isnothing(spec.fixed_cost) && spec.has_status_variable
        @debug "Fixed cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(OnVariable, spec.component_type),
                component_name,
                spec.fixed_cost,
                t,
            )
        end
    end
    return
end

"""
Adds to the models costs represented by PowerSystems ThreePart costs
"""
function add_to_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    cost_data::PSY.ThreePartCost,
    component::PSY.Component,
)
    component_name = PSY.get_name(component)
    @debug "ThreePartCost" component_name
    resolution = model_resolution(psi_container)
    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    variable_cost = PSY.get_variable(cost_data)
    time_steps = model_time_steps(psi_container)
    for t in time_steps
        variable_cost!(psi_container, spec, component_name, variable_cost, t)
    end

    if !isnothing(spec.start_up_cost)
        @debug "Start up cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(StartVariable, spec.component_type),
                component_name,
                spec.start_up_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    if !isnothing(spec.shut_down_cost)
        @debug "Shut down cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(StopVariable, spec.component_type),
                component_name,
                spec.shut_down_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    if !isnothing(spec.fixed_cost) && spec.has_status_variable
        @debug "Fixed cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(OnVariable, spec.component_type),
                component_name,
                spec.fixed_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    return
end

"""
Adds to the models costs represented by PowerSystems Multi-Start costs
"""
function add_to_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    cost_data::PSY.MultiStartCost,
    component::PSY.Component,
)
    component_name = PSY.get_name(component)
    resolution = model_resolution(psi_container)
    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    time_steps = model_time_steps(psi_container)

    if !isnothing(spec.fixed_cost) && spec.has_status_variable
        @debug "Fixed cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(OnVariable, spec.component_type),
                component_name,
                spec.fixed_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    if !isnothing(spec.shut_down_cost)
        @debug "Shut down cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(StopVariable, spec.component_type),
                component_name,
                spec.shut_down_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    # Original implementation had SOS by default
    variable_cost_data = PSY.get_cost(PSY.get_variable(cost_data))
    if !all(iszero.(last.(variable_cost_data)))
        for t in time_steps
            gen_cost =
                pwl_gencost_sos!(psi_container, spec, component_name, variable_cost_data, t)
            add_to_cost_expression!(psi_container, spec.multiplier * gen_cost * dt)
        end
    else
        @debug "No Variable Cost associated with $(component_name)"
    end

    # Start-up costs
    start_cost_data = PSY.get_start_up(cost_data)
    for (st, var_type) in
        enumerate((HotStartVariable, WarmStartVariable, ColdStartVariable))
        var_name = make_variable_name(var_type, spec.component_type)
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                var_name,
                component_name,
                start_cost_data[st] * spec.multiplier,
                t,
            )
        end
    end

    return
end

"""
Adds to the models costs represented by PowerSystems Market-Bid costs
"""
function add_to_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    cost_data::PSY.MarketBidCost,
    component::PSY.Component,
)
    component_name = PSY.get_name(component)
    @debug "Market Bid" component_name
    resolution = model_resolution(psi_container)
    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    time_steps = model_time_steps(psi_container)
    initial_time = model_initial_time(psi_container)
    # TODO: Use mthods from PowerSystems to eliminate this step
    variable_cost_forecast = get_time_series(psi_container, component, "variable_cost")
    variable_cost_forecast = map(PSY.VariableCost, variable_cost_forecast)
    for t in time_steps
        variable_cost!(psi_container, spec, component_name, variable_cost_forecast[t], t)
    end

    if !isnothing(spec.start_up_cost)
        @debug "Start up cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(StartVariable, spec.component_type),
                component_name,
                spec.start_up_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    if spec.has_status_variable
        @debug "no_load cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(OnVariable, spec.component_type),
                component_name,
                PSY.get_no_load(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    if !isnothing(spec.shut_down_cost)
        @debug "Shut down cost" component_name
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                make_variable_name(StopVariable, spec.component_type),
                component_name,
                spec.shut_down_cost(cost_data) * spec.multiplier,
                t,
            )
        end
    end

    #Service Cost Bid
    ancillary_services = PSY.get_ancillary_services(cost_data)
    for service in ancillary_services
        add_service_bid_cost!(psi_container, spec, component, service)
    end
    return
end

function add_service_bid_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component::PSY.Component,
    service::PSY.Service,
)
    return
end

function add_service_bid_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component::PSY.Component,
    service::PSY.Reserve{T},
) where {T <: PSY.ReserveDirection}
    # TODO: Use mthods from PowerSystems to eliminate this step
    forecast_data = get_time_series(psi_container, component, PSY.get_name(service))
    forecast_data = map(PSY.VariableCost, forecast_data)
    time_steps = model_time_steps(psi_container)
    if eltype(forecast_data) == PSY.VariableCost{Float64}
        for t in time_steps
            linear_gen_cost!(
                psi_container,
                spec.addtional_linear_terms[PSY.get_name(service)],
                PSY.get_name(component),
                forecast_data,
                t,
            )
        end
    else
        error("Current version only supports linear cost bid for services, please change the forecast data for $(PSY.get_name(service))")
    end
    return
end

function add_service_bid_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component::PSY.Component,
    service::PSY.ReserveDemandCurve{T},
) where {T <: PSY.ReserveDirection}
    error("Current version doesn't supports cost bid for  ReserveDemandCurve services, please change the forecast data for $(PSY.get_name(service))")
    return
end

@doc raw"""
Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for psi_container model.

    # Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* var_name::Symbol: The variable name
* component_name::String: The component_name of the variable container
* cost_component::PSY.VariableCost{Float64} : container for cost to be associated with variable
"""
function variable_cost!(
    ::PSIContainer,
    ::AddCostSpec,
    component_name::String,
    ::Nothing,
    ::Int,
)
    @debug "Empty Variable Cost" component_name
    return
end

@doc raw"""
Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for psi_container model.

    # Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* var_name::Symbol: The variable name
* component_name::String: The component_name of the variable container
* cost_component::PSY.VariableCost{Float64} : container for cost to be associated with variable
"""
function variable_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component_name::String,
    cost_component::PSY.VariableCost{Float64},
    time_period::Int,
)
    @debug "Linear Variable Cost" component_name
    var_name = make_variable_name(spec.variable_type, spec.component_type)
    cost_data = PSY.get_cost(cost_component)
    linear_gen_cost!(
        psi_container,
        var_name,
        component_name,
        cost_data * spec.multiplier,
        time_period,
    )
    return
end

@doc raw"""
Adds to the cost function cost terms for sum of variables with common factor to be used for cost expression for psi_container model.

# Equation

``` gen_cost = dt*sign*(sum(variable.^2)*cost_data[1] + sum(variable)*cost_data[2]) ```

# LaTeX

`` cost = dt\times sign (sum_{i\in I} c_1 v_i^2 + sum_{i\in I} c_2 v_i ) ``

for quadratic factor large enough. If the first term of the quadratic objective is 0.0, adds a
linear cost term `sum(variable)*cost_data[2]`

# Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* var_name::Symbol: The variable name
* component_name::String: The component_name of the variable container
* cost_component::PSY.VariableCost{NTuple{2, Float64}} : container for quadratic and linear factors
"""
function variable_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component_name::String,
    cost_component::PSY.VariableCost{NTuple{2, Float64}},
    time_period::Int,
)
    var_name = make_variable_name(spec.variable_type, spec.component_type)
    cost_data = PSY.get_cost(cost_component)
    if cost_data[1] >= eps()
        @debug "Quadratic Variable Cost" component_name
        resolution = model_resolution(psi_container)
        dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
        variable = get_variable(psi_container, var_name)[component_name, time_period]
        gen_cost = sum(variable .^ 2) * cost_data[1] + sum(variable) * cost_data[2]
        add_to_cost_expression!(psi_container, spec.multiplier * gen_cost * dt)
    else
        @debug "Quadratic Variable Cost with only linear term" component_name
        linear_gen_cost!(
            psi_container,
            var_name,
            component_name,
            cost_data[2] * spec.multiplier,
            time_period,
        )
    end
    return
end

@doc raw"""
Creates piecewise linear cost function using a sum of variables and expression with sign and time step included.

# Expression

```JuMP.add_to_expression!(gen_cost, c)```

Returns sign*gen_cost*dt

# LaTeX

``cost = sign\times dt \sum_{v\in V} c_v``

where ``c_v`` is given by

`` c_v = \sum_{i\in Ix} \frac{y_i - y_{i-1}}{x_i - x_{i-1}} v^{p.w.}_i ``

# Arguments

* psi_container::PSIContainer : the psi_container model built in PowerSimulations
* var_name::Symbol: The variable name
* component_name::String: The component_name of the variable container
* cost_component::PSY.VariableCost{Vector{NTuple{2, Float64}}}
"""
function variable_cost!(
    psi_container::PSIContainer,
    spec::AddCostSpec,
    component_name::String,
    cost_component::PSY.VariableCost{Vector{NTuple{2, Float64}}},
    time_period::Int,
)
    @debug "PWL Variable Cost" component_name
    resolution = model_resolution(psi_container)
    dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR
    # If array is full of tuples with zeros return 0.0
    cost_data = PSY.get_cost(cost_component)
    if all(iszero.(last.(cost_data)))
        @debug "All cost terms for component $(component_name) are 0.0"
        return JuMP.AffExpr(0.0)
    end

    var_name = make_variable_name(spec.variable_type, spec.component_type)
    if !pwlparamcheck(cost_component)
        @warn("The cost function provided for $(var_name) device is not compatible with a linear PWL cost function.
        An SOS-2 formulation will be added to the model.
        This will result in additional binary variables added to the model.")
        gen_cost =
            pwl_gencost_sos!(psi_container, spec, component_name, cost_data, time_period)
    else
        gen_cost =
            pwl_gencost_linear!(psi_container, spec, component_name, cost_data, time_period)
    end
    add_to_cost_expression!(psi_container, spec.multiplier * gen_cost * dt)
    return
end


using DataStructures: OrderedDict, PriorityQueue
import DataStructures: peek, dequeue!

import Base: length, isempty, push!

"""
    MemoryJobStore()

`MemoryJobStore` implements `AbstractJobStore`.

This is a data structure which is responsible of
storing into memory jobs that should be executed later.
"""
struct MemoryJobStore <: AbstractJobStore
    d::OrderedDict
    q::PriorityQueue

    MemoryJobStore() = new(
        OrderedDict{String,Job}(),
        PriorityQueue{String, Priority}(Base.Order.Reverse)
    )
end

"""
    length(jobstore)

Return the number of jobs stored into jobstore.

A given job can be executed several times
(periodical jobs for example).
"""
length(jobstore::MemoryJobStore) = length(jobstore.d)

"""
    isempty(jobstore)

Returns `true` when there isn't any job in `jobstore`.
"""
isempty(jobstore::MemoryJobStore) = length(jobstore) == 0
function push!(jobstore::MemoryJobStore, job::Job)
    if hasjob(jobstore, job.id)
        error("Can't add a job with same job_id")
    else
        schedule(jobstore, job, true)
    end
end

"""
    schedule(jobstore, job, first_time)

Schedule a job according its `Priority`.
"""
function schedule(jobstore::MemoryJobStore, job::Job, first_time::Bool)
    now_ = job.dt_updated
    # Calculate the next fire time if there is none defined
    if !hasnextfire(job) || !first_time
        job.dt_next_fire = get_next_dt_fire(job.trigger, DateTime(0), now_)
    end
    jobstore.d[job.id] = job
    priority = Priority(job.dt_next_fire, job.priority)
    jobstore.q[job.id] = priority
    println("[+] $now_ Schedule job $(job.id)")
    dump(job)
    println()
end

"""
    job = peek(jobstore)

Return the next `job` from a `jobstore` without removing it from this `jobstore`.
"""
function peek(jobstore::MemoryJobStore)
    job_id, priority = peek(jobstore.q)
    job = jobstore.d[job_id]
    job
end

"""
    job = dequeue!(jobstore)

Remove and return the next `job` from a `jobstore`.
"""
function dequeue!(jobstore::MemoryJobStore)
    job_id = dequeue!(jobstore.q)
    jobstore.d[job_id]
end

"""
    hasjob(jobstore, job_id)

Return `true` if a job with identifier `job_id`
is stored into `jobstore`.
"""
function hasjob(jobstore::MemoryJobStore, job_id::String)
    job_id in keys(jobstore.d)
end

"""
    get_job_id(jobstore)

Returns a job identifier (a `job_id`).

Parameter `jobstore` is passed to ensure that
no other job stored in `jobstore` have same `job_id`.
"""
function get_job_id(jobstore::MemoryJobStore)
    job_id = ""
    while job_id == "" || hasjob(jobstore, job_id)
        job_id = get_job_id()
    end
    job_id
end

"""
    update(jobstore, job, now_)

Update `jobstore` for a given `job`.
Schedule when a `job` should be fire or delete it from `jobstore`
if it shouldn't be fired again.
"""
function update(jobstore::MemoryJobStore, job::Job, now_)
    job.dt_updated = now_
    itr_size = IteratorSize(job.trigger)
    if itr_size == HasLength()
        remaining = length(job.trigger) - job.n_triggered
        if remaining > 0
            schedule(jobstore, job, false)
        elseif remaining == 0
            delete!(jobstore.d, job.id)
        else
            error("remaining should be positive or no")
        end
    elseif itr_size == IsInfinite()
        schedule(jobstore, job, false)
    else
        error("undefined iterator size $itr_size")
    end
end


endian(::Type{C}) where {T, fr, fg, fb, en, C <: AbstractStorageRGB{T, fr, fg, fb, en}} = en
nbitsfr(::Type{C}) where {T, fr, fg, fb, C <: AbstractStorageRGB{T, fr, fg, fb}} = fr
nbitsfg(::Type{C}) where {T, fr, fg, fb, C <: AbstractStorageRGB{T, fr, fg, fb}} = fg
nbitsfb(::Type{C}) where {T, fr, fg, fb, C <: AbstractStorageRGB{T, fr, fg, fb}} = fb
nshiftr(::Type{C}) where {C} = nbitsfg(C) + nshiftg(C)
nshiftg(::Type{C}) where {C} = nbitsfb(C) + nshiftb(C)
nshiftb(::Type{C}) where {T, fr, fg, fb, C <: AbstractStorageRGB{T, fr, fg, fb}} = 0
nshiftb(::Type{C}) where {C <: Union{RGBX444LE, RGBX444BE}} = 4
rawtype(::Type{C}) where {C <: AbstractStorageRGB} =
    sizeof(C) == 1 ? UInt8 : sizeof(C) == 2 ? UInt16 : UInt32
saturatedr(::Type{C}) where {C <: AbstractStorageRGB} = ~(typemax(rawtype(C)) << nbitsfr(C))
saturatedg(::Type{C}) where {C <: AbstractStorageRGB} = ~(typemax(rawtype(C)) << nbitsfg(C))
saturatedb(::Type{C}) where {C <: AbstractStorageRGB} = ~(typemax(rawtype(C)) << nbitsfb(C))
maskr(::Type{C}) where {C <: AbstractStorageRGB} = saturatedr(C) << nshiftr(C)
maskg(::Type{C}) where {C <: AbstractStorageRGB} = saturatedg(C) << nshiftg(C)
maskb(::Type{C}) where {C <: AbstractStorageRGB} = saturatedb(C) << nshiftb(C)

endian(::C) where {C <: AbstractStorageRGB} = endian(C)
nbitsfr(::C) where {C <: AbstractStorageRGB} = nbitsfr(C)
nbitsfg(::C) where {C <: AbstractStorageRGB} = nbitsfg(C)
nbitsfb(::C) where {C <: AbstractStorageRGB} = nbitsfb(C)
nshiftr(::C) where {C <: AbstractStorageRGB} = nshiftr(C)
nshiftg(::C) where {C <: AbstractStorageRGB} = nshiftg(C)
nshiftb(::C) where {C <: AbstractStorageRGB} = nshiftb(C)
saturatedr(::C) where {C <: AbstractStorageRGB} = saturatedr(C)
saturatedg(::C) where {C <: AbstractStorageRGB} = saturatedg(C)
saturatedb(::C) where {C <: AbstractStorageRGB} = saturatedb(C)
maskr(::C) where {C <: AbstractStorageRGB} = maskr(C)
maskg(::C) where {C <: AbstractStorageRGB} = maskg(C)
maskb(::C) where {C <: AbstractStorageRGB} = maskb(C)

toh(c::C) where {C <: AbstractStorageRGB} = (endian(C) === :be ? ntoh : ltoh)(c.color)

# Accessors
red(c::Union{RGB565LE, RGB565BE, RGB555LE, RGB555BE}) =
    n3f5ton0f8((toh(c) >> nshiftr(c)) & saturatedr(c))
red(c::Union{XRGB444LE, XRGB444BE, RGBX444LE, RGBX444BE}) =
    n4f4ton0f8((toh(c) >> nshiftr(c)) & saturatedr(c))

green(c::Union{RGB565LE, RGB565BE}) = n2f6ton0f8((toh(c) >> nshiftg(c)) & saturatedg(c))
green(c::Union{RGB555LE, RGB555BE}) = n3f5ton0f8((toh(c) >> nshiftg(c)) & saturatedg(c))
green(c::Union{XRGB444LE, XRGB444BE, RGBX444LE, RGBX444BE}) =
    n4f4ton0f8((toh(c) >> nshiftg(c)) & saturatedg(c))

blue(c::Union{RGB565LE, RGB565BE, RGB555LE, RGB555BE}) =
    n3f5ton0f8((toh(c) >> nshiftb(c)) & saturatedb(c))
blue(c::Union{XRGB444LE, XRGB444BE, RGBX444LE, RGBX444BE}) =
    n4f4ton0f8((toh(c) >> nshiftb(c)) & saturatedb(c))

chroma(c::Union{Lab24, Luv24}) = sqrt(comp2(c)^2 + comp3(c)^2)

hue(c::Union{HSV24, HSL24, HLS24, HLS}) = comp1(c)
hue(c::Union{Lab24, Luv24}) = (h = atand(comp3(c), comp2(c)); h < 0 ? h + 360 : h)

comp1(c::Union{HSV24, HSL24, HLS24}) = getfield(c, :h_raw) * 2.0f0
comp1(c::Union{Lab24, Luv24}) = getfield(c, :l_raw) * 65537f0 * Float32(0x1919p-30)
comp2(c::Union{HSV24, HSL24, HLS24}) = @inbounds n0f8tof32[getfield(c, :s_raw) + 1]
comp2(c::Union{Lab24}) = getfield(c, :a_raw) - 128f0
comp2(c::Union{Luv24}) = getfield(c, :u_raw) * (354f0 / 255f0) - 134f0
comp2(c::HLS) = c.s
comp3(c::Union{HSV24}) = @inbounds n0f8tof32[getfield(c, :v_raw) + 1]
comp3(c::Union{HSL24, HLS24}) = @inbounds n0f8tof32[getfield(c, :l_raw) + 1]
comp3(c::Union{Lab24}) = getfield(c, :b_raw) - 128f0
comp3(c::Union{Luv24}) = getfield(c, :v_raw) * (262f0 / 255f0) - 140f0
comp3(c::HLS) = c.l

function Base.getproperty(c::HSV24, sym::Symbol)
    sym === :h && return comp1(c)
    sym === :s && return comp2(c)
    sym === :v && return comp3(c)
    getfield(c, sym)
end

function Base.getproperty(c::HSL24, sym::Symbol)
    sym === :h && return comp1(c)
    sym === :s && return comp2(c)
    sym === :l && return comp3(c)
    getfield(c, sym)
end

function Base.getproperty(c::HLS24, sym::Symbol)
    sym === :h && return comp1(c)
    sym === :l && return comp2(c)
    sym === :s && return comp3(c)
    getfield(c, sym)
end

floattype(::Type{HSV24}) = HSV{Float32}
floattype(::Type{HSL24}) = HSL{Float32}
floattype(::Type{HLS24}) = HLS{Float32}


"""
`module  JAC.DoubleAuger`  ... a submodel of JAC that contains all methods for computing double Auger and autoionization amplitudes
                               and rates; it is using JAC, JAC.ManyElectron, JAC.ImpactExcitation, JAC.AutoIonization.
"""
module DoubleAuger

    using JAC, JAC.ManyElectron, JAC.ImpactExcitation, JAC.AutoIonization

    """
    `struct  DoubleAuger.Settings`  ... defines a type for the settings in estimating double-Auger and autoionization rates.

        + printBeforeComputation  ::Bool                         ... True, if all energies and lines are printed before their evaluation.
        + selectLines             ::Bool                         ... True, if lines are selected individually for the computations.
        + selectedLines           ::Array{Tuple{Int64,Int64},1}  ... List of lines, given by tupels (inital-level, final-level).
        + minAugerEnergy          ::Float64                      ... Minimum energy of free (Auger) electrons to be included.
        + maxAugerEnergy          ::Float64                      ... Maximum energy of free (Auger) electrons to be included.
        + maxKappa                ::Int6464                      ... Maximum kappa value of partial waves to be included.
    """
    struct Settings
        printBeforeComputation    ::Bool
        selectLines               ::Bool
        selectedLines             ::Array{Tuple{Int64,Int64},1}
        minAugerEnergy            ::Float64
        maxAugerEnergy            ::Float64
        maxKappa                  ::Int64
    end

    """
    `JAC.DoubleAuger.Settings()`  ... constructor for the default values of DoubleAuger line computations
    """
    function Settings()
        Settings(false, false, Array{Tuple{Int64,Int64},1}[], 0., 10e5, 100)
    end

    # `Base.show(io::IO, settings::DoubleAuger.Settings)`  ... prepares a proper printout of the variable settings::DoubleAuger.Settings.
    function Base.show(io::IO, settings::DoubleAuger.Settings)
        println(io, "printBeforeComputation:       $(settings.printBeforeComputation)  ")
        println(io, "selectLines:                  $(settings.selectLines)  ")
        println(io, "selectedLines:                $(settings.selectedLines)  ")
        println(io, "minAugerEnergy:               $(settings.minAugerEnergy)  ")
        println(io, "maxAugerEnergy:               $(settings.maxAugerEnergy)  ")
        println(io, "maxKappa:                     $(settings.maxKappa)  ")
    end

    """
    `struct  Channel`  ... defines a type for a DoubleAuger channel to help characterize a scattering (continuum) state of many
                           electron-states with two free electrons.

        + kappa1         ::Int64                ... partial-wave of the free electron_1
        + kappa2         ::Int64                ... partial-wave of the free electron_2
        + symmetry       ::LevelSymmetry        ... total angular momentum and parity of the scattering state
        + phase1         ::Float64              ... phase of the partial wave_1
        + phase2         ::Float64              ... phase of the partial wave_2
        + amplitude      ::Complex{Float64}     ... Auger amplitude associated with the given channel.
    """
    struct  Channel
        kappa1           ::Int64
        kappa2           ::Int64
        symmetry         ::LevelSymmetry
        phase1           ::Float64
        phase2           ::Float64
        amplitude        ::Complex{Float64}
    end

    """
    `struct  Line`  ... defines a type for a Auger line that may include the definition of sublines and their corresponding amplitudes.

        + initialLevel   ::Level         ... initial-(state) level
        + finalLevel     ::Level         ... final-(state) level
        + electronEnergy ::Float64       ... Energy of the (incoming free) electron.
        + totalRate      ::Float64       ... Total rate of this line.
        + hasChannels    ::Bool          ... Determines whether the individual scattering (sub-) channels are defined in terms of their free-
                                             electron energies, kappas and the total angular momentum/parity as well as the amplitude, or not.
        + channels       ::Array{DoubleAuger.Channel,1}  ... List of DoubleAuger channels of this line.
    """
    struct  Line
        initialLevel     ::Level
        finalLevel       ::Level
        electronEnergy   ::Float64
        totalRate        ::Float64
        hasChannels      ::Bool
        channels         ::Array{DoubleAuger.Channel,1}
    end

    # `Base.show(io::IO, line::DoubleAuger.Line)`  ... prepares a proper printout of the variable line::DoubleAuger.Line.
    function Base.show(io::IO, line::DoubleAuger.Line)
        println(io, "initialLevel:           $(line.initialLevel)  ")
        println(io, "finalLevel:             $(line.finalLevel)  ")
        println(io, "electronEnergy:         $(line.electronEnergy)  ")
        println(io, "totalRate:              $(line.totalRate)  ")
        println(io, "hasChannels:            $(line.hasChannels)  ")
        println(io, "channels:               $(line.channels)  ")
    end

end # module


#################################################################################
#Type Alias for long type signatures
const MinMax = NamedTuple{(:min, :max), NTuple{2, Float64}}
const NamedMinMax = Tuple{String, MinMax}
const UpDown = NamedTuple{(:up, :down), NTuple{2, Float64}}
const InOut = NamedTuple{(:in, :out), NTuple{2, Float64}}
const StartUpStages = NamedTuple{(:hot, :warm, :cold), NTuple{3, Float64}}

# Type Alias From other Packages
const PM = PowerModels
const PSY = PowerSystems
const PSI = PowerSimulations
const IS = InfrastructureSystems
const MOI = MathOptInterface
const MOIU = MathOptInterface.Utilities
const PJ = ParameterJuMP
const MOPFM = MOI.FileFormats.Model
const TS = TimeSeries

const BUILD_SIMULATION_TIMER = TimerOutputs.TimerOutput()
const RUN_SIMULATION_TIMER = TimerOutputs.TimerOutput()

#Type Alias for JuMP and PJ containers
const JuMPExpressionMatrix = Matrix{<:JuMP.AbstractJuMPScalar}
const PGAE = PJ.ParametrizedGenericAffExpr{Float64, JuMP.VariableRef}
const GAE = JuMP.GenericAffExpr{Float64, JuMP.VariableRef}
const JuMPAffineExpressionArray = Matrix{GAE}
const JuMPAffineExpressionVector = Vector{GAE}
const JuMPConstraintArray = JuMP.Containers.DenseAxisArray{JuMP.ConstraintRef}
const JuMPVariableArray = JuMP.Containers.DenseAxisArray{JuMP.VariableRef}
const JuMPParamArray = JuMP.Containers.DenseAxisArray{PJ.ParameterRef}
const DenseAxisArrayContainer = Dict{Symbol, JuMP.Containers.DenseAxisArray}

@enum BUILD_STATUS begin
    BUILT = 1
    IN_PROGRESS = -1
    EMPTY = 0
end

@enum SOS_STATUS_VARIABLE begin
    NO_VARIABLE = 1
    PARAMETER = 2
    VARIABLE = 3
end

@enum STAGE_STATUS begin
    SUCESSFUL_RUN = 0
    FAILED_RUN = 1
end

# Settings constants
const UNSET_HORIZON = 0
const UNSET_INI_TIME = Dates.DateTime(0)

# Tolerance of comparisons
# MIP gap tolerances in most solvers are set to 1e-4
const ABSOLUTE_TOLERANCE = 1.0e-3
const BALANCE_SLACK_COST = 1e6
const SERVICES_SLACK_COST = 1e5
const COST_EPSILON = 1e-3
const MISSING_INITIAL_CONDITIONS_TIME_COUNT = 999.0
const SECONDS_IN_MINUTE = 60.0
const MINUTES_IN_HOUR = 60.0
const SECONDS_IN_HOUR = 3600.0
const MAX_START_STAGES = 3
const OBJECTIVE_FUNCTION_POSITIVE = 1.0
const OBJECTIVE_FUNCTION_NEGATIVE = -1.0
# The DEFAULT_RESERVE_COST value is used to avoid degeneracy of the solutions, reseve cost isn't provided.
const DEFAULT_RESERVE_COST = 1.0e-4

# Interface limitations
const OPERATIONS_ACCEPTED_KWARGS = [
    :horizon,
    :initial_time,
    :use_forecast_data,
    :PTDF,
    :use_parameters,
    :optimizer,
    :warm_start,
    :balance_slack_variables,
    :services_slack_variables,
    :system_to_file,
    :constraint_duals,
    :export_pwl_vars,
]

const OPERATIONS_SOLVE_KWARGS = [:optimizer, :save_path]

const STAGE_ACCEPTED_KWARGS = [
    :PTDF,
    :warm_start,
    :balance_slack_variables,
    :services_slack_variables,
    :constraint_duals,
    :system_to_file,
    :export_pwl_vars,
    :allow_fails,
]

const UNSUPPORTED_POWERMODELS =
    [PM.SOCBFPowerModel, PM.SOCBFConicPowerModel, PM.IVRPowerModel]

const PSI_NAME_DELIMITER = "__"

const M_VALUE = 1e6


    # Functions and methods for cascade computation

    """
    `Cascade.computeSteps(scheme::Cascade.StepwiseDecayScheme, comp::Cascade.Computation, stepList::Array{Cascade.Step,1})`
        ... computes in turn all the requested transition amplitudes as well as PhotoEmission.Line's, AutoIonization.Line's,
            etc. for all pre-specified decay steps of the cascade. When compared with standard computations of these atomic
            processes, however, the amount of output is largely reduced and often just printed into the summary file.
            A set of  data::Cascade.DecayData  is returned.
    """
    function computeSteps(scheme::Cascade.StepwiseDecayScheme, comp::Cascade.Computation, stepList::Array{Cascade.Step,1})
        linesA = AutoIonization.Line[];    linesR = PhotoEmission.Line[];    cOrbitals = Dict{Subshell, Orbital}()
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        nt = 0;   st = 0;   previousMeanEn = 0.
        for  step  in  stepList
            st = st + 1
            nc = length(step.initialMultiplet.levels) * length(step.finalMultiplet.levels)
            println("\n  $st) Perform $(string(step.process)) amplitude computations for up to $nc decay lines (without selection rules): ")
            if  printSummary   println(iostream, "\n* $st) Perform $(string(step.process)) amplitude computations for " *
                                                 "up to $nc decay lines (without selection rules): ")   end

            if      step.process == Basics.Auger()  &&   comp.approach == Cascade.AverageSCA()
                # First determine the `mean' free-electron energy for this Auger block and calculate a common set of continuum orbital
                meanEn = 0.;    NoEn = 0
                for  p = 1:length(step.initialMultiplet.levels),  q = 1:length(step.finalMultiplet.levels)
                    en = step.initialMultiplet.levels[p].energy - step.finalMultiplet.levels[q].energy
                    if  en > 0.1    meanEn = meanEn + en;    NoEn = NoEn + 1    end
                end
                if  NoEn > 0     meanEn = meanEn/ NoEn     else     meanEn = 0.1    end
                if  abs(meanEn - previousMeanEn) / meanEn  < 0.15   # no new continuum orbitals
                    println(">> No new continum orbitals are generated for $(keys(cOrbitals)) and for the energy $meanEn ")
                else
                    previousMeanEn = meanEn;    cOrbitals = Dict{Subshell, Orbital}()
                    for kappa = -step.settings.maxKappa-1:step.settings.maxKappa        if   kappa == 0     continue    end
                        sh           = Subshell(101, kappa);     nrContinuum = Continuum.gridConsistency(meanEn, comp.grid)
                        contSettings = Continuum.Settings(false, nrContinuum);
                        npot         = Nuclear.nuclearPotential(comp.nuclearModel, comp.grid)
                        ## wp1 = compute("radial potential: core-Hartree", grid, wLevel)
                        ## wp2 = compute("radial potential: Hartree-Slater", grid, wLevel)
                        ## wp3 = compute("radial potential: Kohn-Sham", grid, wLevel)
                        ## wp           = Basics.compute("radial potential: Dirac-Fock-Slater", comp.grid, step.finalMultiplet.levels[1].basis)
                        wp           = Basics.computePotentialDFS(comp.grid, step.finalMultiplet.levels[1])
                        pot          = Basics.add(npot, wp)
                        cOrbital, phase, normF  = Continuum.generateOrbitalLocalPotential(meanEn, sh, pot, contSettings)
                        cOrbitals[sh] = cOrbital
                    end
                    println(">> Generate continum orbitals in DFS potential for $(keys(cOrbitals)) and for the energy $meanEn ")
                end

                newLines = AutoIonization.computeLinesFromOrbitals(step.finalMultiplet, step.initialMultiplet, comp.nuclearModel, comp.grid,
                                                                   step.settings, cOrbitals, output=true, printout=false)
                append!(linesA, newLines);    nt = length(linesA)
            elseif  step.process == Basics.Auger()
                # Compute continuum orbitals independently for all transitions in the given block.
                newLines = AutoIonization.computeLinesCascade(step.finalMultiplet, step.initialMultiplet, comp.nuclearModel, comp.grid,
                                                              step.settings, output=true, printout=false)
                append!(linesA, newLines);    nt = length(linesA)
            elseif  step.process == Basics.Radiative()
                newLines = PhotoEmission.computeLinesCascade(step.finalMultiplet, step.initialMultiplet, comp.grid,
                                                             step.settings, output=true, printout=false)
                append!(linesR, newLines);    nt = length(linesR)
            else   error("Unsupported atomic process for cascade computations.")
            end
            println("     Step $st:: A total of $(length(newLines)) $(string(step.process)) lines are calculated, giving now rise " *
                    "to a total of $nt $(string(step.process)) decay lines." )
            if  printSummary   println(iostream, "\n*    Step $st:: A total of $(length(newLines)) $(string(step.process)) lines are calculated, " *
                                                 "giving now rise to a total of $nt $(string(step.process)) decay lines." )   end
        end
        #
        data = Cascade.DecayData(linesR, linesA)
    end

    """
    `Cascade.determineSteps(scheme::Cascade.StepwiseDecayScheme, comp::Cascade.Computation, blockList::Array{Cascade.Block,1})`
        ... determines all step::Cascade.Step's that need to be computed for this decay cascade. It cycles through all processes of the given
            decay scheme and selects all pairs of blocks due to the selected cascade approach. It is checked that the (averaged) energies
            each block or level supports a `decay' within the step. A stepList::Array{Cascade.Step,1} is returned, and for which subsequently
            all required transition amplitudes and rates are computed.
    """
    function determineSteps(scheme::Cascade.StepwiseDecayScheme, comp::Cascade.Computation, blockList::Array{Cascade.Block,1})
        stepList = Cascade.Step[]
        if  comp.approach  in  [Cascade.AverageSCA(), Cascade.SCA()]
            for  a = 1:length(blockList)
                for  b = 1:length(blockList)
                    minEn = 100000.;   maxEn = -100000.
                    for  p = 1:length(blockList[a].multiplet.levels),  q = 1:length(blockList[b].multiplet.levels)
                        minEn = min(minEn, blockList[a].multiplet.levels[p].energy - blockList[b].multiplet.levels[q].energy)
                        maxEn = max(maxEn, blockList[a].multiplet.levels[p].energy - blockList[b].multiplet.levels[q].energy)
                    end
                    for  process  in  comp.scheme.processes
                        if      process == Basics.Radiative()
                            if  a == b   ||   minEn < 0.    continue   end
                            if  blockList[a].NoElectrons == blockList[b].NoElectrons
                                settings = PhotoEmission.Settings([E1], [UseBabushkin], false, false, LineSelection(), 0., 0., 1.0e6)
                                push!( stepList, Cascade.Step(process, settings, blockList[a].confs, blockList[b].confs,
                                                              blockList[a].multiplet, blockList[b].multiplet) )
                            end
                        elseif  process == Basics.Auger()
                            if  a == b   ||   minEn < 0.    continue   end
                            if  blockList[a].NoElectrons == blockList[b].NoElectrons + 1
                                settings = AutoIonization.Settings(false, false, LineSelection(), 0., 1.0e6, 3, CoulombInteraction())
                                push!( stepList, Cascade.Step(process, settings, blockList[a].confs, blockList[b].confs,
                                                              blockList[a].multiplet, blockList[b].multiplet) )
                            end
                        else    error("stop a")
                        end
                    end
                end
            end
            #
        else  error("Unsupported cascade approach.")
        end
        return( stepList )
    end

    """
    `Cascade.perform(scheme::StepwiseDecayScheme, comp::Cascade.Computation)`
        ... to set-up and perform a cascade computation that starts from a given set of initial configurations and proceeds via
            various steps until a given number of electrons has been removed or the decay stops at some stable levels with regard
            to the given atomic processes. The results of all individual steps are printed to screen but nothing is returned
            otherwise.

    `Cascade.perform(scheme::StepwiseDecayScheme, comp::Cascade.Computation; output::Bool=false, outputToFile::Bool=true)`
        ... to perform the same but to return the complete output in a dictionary;  the particular output depends on the type
            and specifications of the cascade but can easily accessed by the keys of this dictionary.
    """
    function perform(scheme::StepwiseDecayScheme, comp::Cascade.Computation; output::Bool=false, outputToFile::Bool=true)
        if  output    results = Dict{String, Any}()    else    results = nothing    end
        printSummary, iostream = Defaults.getDefaults("summary flag/stream")
        #
        # Perform the SCF and CI computation for the intial-state multiplets if initial configurations are given
        if  comp.initialConfigs != Configuration[]
            basis      = Basics.performSCF(comp.initialConfigs, comp.nuclearModel, comp.grid, comp.asfSettings; printout=false)
            multiplet  = Basics.performCI(basis, comp.nuclearModel, comp.grid, comp.asfSettings; printout=false)
            multiplets = [Multiplet("initial states", multiplet.levels)]
        else
            basis      = comp.initialMultiplets[1].levels[1].basis
            multiplets = comp.initialMultiplets
        end
        # Print out initial configurations and levels
        Cascade.displayLevels(stdout, multiplets, sa="initial ")
        if  printSummary   Cascade.displayLevels(iostream, multiplets, sa="initial ")          end
        #
        # Generate subsequent cascade configurations as well as display and group them together
        wa = Cascade.generateConfigurationList(multiplets, comp.scheme.maxElectronLoss, comp.scheme.NoShakeDisplacements)
        wb = Cascade.groupDisplayConfigurationList(comp.nuclearModel.Z, wa, sa="decay ")
        #
        # Determine first all configuration 'blocks' and from them the individual steps of the cascade
        wc = Cascade.generateBlocks(comp, wb, basis.orbitals, sa="for the decay cascade:")
        Cascade.displayBlocks(stdout, wc, sa="for the decay cascade ")
        if  printSummary   Cascade.displayBlocks(iostream, wc, sa="for the decay cascade ")    end
        gMultiplets = Multiplet[];     for block in wc  push!(gMultiplets, block.multiplet)    end
        # Determine, modify and compute the transition data for all steps, ie. the PhotoEmission.Line's, the AutoIonization.Line's, etc.
        wd = Cascade.determineSteps(scheme, comp, wc)
        Cascade.displaySteps(stdout, wd, sa="decay ")
        if  printSummary   Cascade.displaySteps(iostream, wd, sa="decay ")    end
        we   = Cascade.modifySteps(wd)
        @time data = Cascade.computeSteps(scheme, comp, we)
        if output
            results = Base.merge( results, Dict("name"                  => comp.name) )
            results = Base.merge( results, Dict("cascade scheme"        => comp.scheme) )
            results = Base.merge( results, Dict("initial multiplets:"   => multiplets) )
            results = Base.merge( results, Dict("generated multiplets:" => gMultiplets) )
            results = Base.merge( results, Dict("decay line data:"      => data) )
            #
            #  Write out the result to file to later continue with simulations on the cascade data
            if outputToFile
                filename = "zzz-cascade-decay-computations-" * string(Dates.now())[1:13] * ".jld"
                println("\n* Write all results to disk; use:\n   JLD.save(''$filename'', results) \n   using JLD " *
                        "\n   results = JLD.load(''$filename'')    ... to load the results back from file.")
                if  printSummary   println(iostream, "\n* Write all results to disk; use:\n   JLD.save(''$filename'', results) \n   using JLD " *
                                                     "\n   results = JLD.load(''$filename'')    ... to load the results back from file." )      end
                ## JLD.save(filename, results)
                JLD2.@save filename results
            end
        else    results = nothing
        end

        return( results )
    end


function get防区编号和FL(sc,t0)
   global    CalChn1_2Obj
    meters    =  CalChn1_2Obj.CfgInfoObj.meterperSeg
   segIDChn2 =  CalChn1_2Obj.CfgInfoObj.chn2SegBegIdx
   chnid = 1
   fl = 0
   segid = floor(Int, sc/10)
   if segid >= segIDChn2
        chnid =2
        fl = (segid - segIDChn2 )  * meters
    else
        chnid = 1
        fl = (segid )  * meters
    end
    return ( chnid,fl , UInt2DTStr( t0 ))
# @info "----SegParaAlarmProc.CalChn1_2Obj.CfgInfoObj.meterperSeg-- ???:",SegParaAlarmProc.CalChn1_2Obj.CfgInfoObj.meterperSeg
end