# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.

using Printf

bin(x) = [xi > 0.5 for xi in x]

function validate(instance_filename::String, solution_filename::String)
    instance = UnitCommitment.read(instance_filename)
    solution = JSON.parse(open(solution_filename))
    return validate(instance, solution)
end

"""
    validate(instance, solution)::Bool

Verifies that the given solution is feasible for the problem. If feasible,
silently returns true. In infeasible, returns false and prints the validation
errors to the screen.

This function is implemented independently from the optimization model in
`model.jl`, and therefore can be used to verify that the model is indeed
producing valid solutions. It can also be used to verify the solutions produced
by other optimization packages.
"""
function validate(
    instance::UnitCommitmentInstance,
    solution::Union{Dict,OrderedDict},
)::Bool
    err_count = 0
    err_count += _validate_units(instance, solution)
    err_count += _validate_reserve_and_demand(instance, solution)

    if err_count > 0
        @error "Found $err_count validation errors"
        return false
    end

    return true
end

function _validate_units(instance, solution; tol = 0.01)
    err_count = 0

    for unit in instance.units
        production = solution["Production (MW)"][unit.name]
        reserve = solution["Reserve (MW)"][unit.name]
        actual_production_cost = solution["Production cost (\$)"][unit.name]
        actual_startup_cost = solution["Startup cost (\$)"][unit.name]
        is_on = bin(solution["Is on"][unit.name])

        for t in 1:instance.time
            # Auxiliary variables
            if t == 1
                is_starting_up = (unit.initial_status < 0) && is_on[t]
                is_shutting_down = (unit.initial_status > 0) && !is_on[t]
                ramp_up =
                    max(0, production[t] + reserve[t] - unit.initial_power)
                ramp_down = max(0, unit.initial_power - production[t])
            else
                is_starting_up = !is_on[t-1] && is_on[t]
                is_shutting_down = is_on[t-1] && !is_on[t]
                ramp_up = max(0, production[t] + reserve[t] - production[t-1])
                ramp_down = max(0, production[t-1] - production[t])
            end

            # Compute production costs
            production_cost, startup_cost = 0, 0
            if is_on[t]
                production_cost += unit.min_power_cost[t]
                residual = max(0, production[t] - unit.min_power[t])
                for s in unit.cost_segments
                    cleared = min(residual, s.mw[t])
                    production_cost += cleared * s.cost[t]
                    residual = max(0, residual - s.mw[t])
                end
            end

            # Production should be non-negative
            if production[t] < -tol
                @error @sprintf(
                    "Unit %s produces negative amount of power at time %d (%.2f)",
                    unit.name,
                    t,
                    production[t]
                )
                err_count += 1
            end

            # Verify must-run
            if !is_on[t] && unit.must_run[t]
                @error @sprintf(
                    "Must-run unit %s is offline at time %d",
                    unit.name,
                    t
                )
                err_count += 1
            end

            # Verify reserve eligibility
            if !unit.provides_spinning_reserves[t] && reserve[t] > tol
                @error @sprintf(
                    "Unit %s is not eligible to provide spinning reserves at time %d",
                    unit.name,
                    t
                )
                err_count += 1
            end

            # If unit is on, must produce at least its minimum power
            if is_on[t] && (production[t] < unit.min_power[t] - tol)
                @error @sprintf(
                    "Unit %s produces below its minimum limit at time %d (%.2f < %.2f)",
                    unit.name,
                    t,
                    production[t],
                    unit.min_power[t]
                )
                err_count += 1
            end

            # If unit is on, must produce at most its maximum power
            if is_on[t] &&
               (production[t] + reserve[t] > unit.max_power[t] + tol)
                @error @sprintf(
                    "Unit %s produces above its maximum limit at time %d (%.2f + %.2f> %.2f)",
                    unit.name,
                    t,
                    production[t],
                    reserve[t],
                    unit.max_power[t]
                )
                err_count += 1
            end

            # If unit is off, must produce zero
            if !is_on[t] && production[t] + reserve[t] > tol
                @error @sprintf(
                    "Unit %s produces power at time %d while off",
                    unit.name,
                    t
                )
                err_count += 1
            end

            # Startup limit
            if is_starting_up && (ramp_up > unit.startup_limit + tol)
                @error @sprintf(
                    "Unit %s exceeds startup limit at time %d (%.2f > %.2f)",
                    unit.name,
                    t,
                    ramp_up,
                    unit.startup_limit
                )
                err_count += 1
            end

            # Shutdown limit
            if is_shutting_down && (ramp_down > unit.shutdown_limit + tol)
                @error @sprintf(
                    "Unit %s exceeds shutdown limit at time %d (%.2f > %.2f)",
                    unit.name,
                    t,
                    ramp_down,
                    unit.shutdown_limit
                )
                err_count += 1
            end

            # Ramp-up limit
            if !is_starting_up &&
               !is_shutting_down &&
               (ramp_up > unit.ramp_up_limit + tol)
                @error @sprintf(
                    "Unit %s exceeds ramp up limit at time %d (%.2f > %.2f)",
                    unit.name,
                    t,
                    ramp_up,
                    unit.ramp_up_limit
                )
                err_count += 1
            end

            # Ramp-down limit
            if !is_starting_up &&
               !is_shutting_down &&
               (ramp_down > unit.ramp_down_limit + tol)
                @error @sprintf(
                    "Unit %s exceeds ramp down limit at time %d (%.2f > %.2f)",
                    unit.name,
                    t,
                    ramp_down,
                    unit.ramp_down_limit
                )
                err_count += 1
            end

            # Verify startup costs & minimum downtime
            if is_starting_up

                # Calculate how much time the unit has been offline
                time_down = 0
                for k in 1:(t-1)
                    if !is_on[t-k]
                        time_down += 1
                    else
                        break
                    end
                end
                if (t == time_down + 1) && (unit.initial_status < 0)
                    time_down -= unit.initial_status
                end

                # Calculate startup costs
                for c in unit.startup_categories
                    if time_down >= c.delay
                        startup_cost = c.cost
                    end
                end

                # Check minimum downtime
                if time_down < unit.min_downtime
                    @error @sprintf(
                        "Unit %s violates minimum downtime at time %d",
                        unit.name,
                        t
                    )
                    err_count += 1
                end
            end

            # Verify minimum uptime
            if is_shutting_down

                # Calculate how much time the unit has been online
                time_up = 0
                for k in 1:(t-1)
                    if is_on[t-k]
                        time_up += 1
                    else
                        break
                    end
                end
                if (t == time_up + 1) && (unit.initial_status > 0)
                    time_up += unit.initial_status
                end

                # Check minimum uptime
                if time_up < unit.min_uptime
                    @error @sprintf(
                        "Unit %s violates minimum uptime at time %d",
                        unit.name,
                        t
                    )
                    err_count += 1
                end
            end

            # Verify production costs
            if abs(actual_production_cost[t] - production_cost) > 1.00
                @error @sprintf(
                    "Unit %s has unexpected production cost at time %d (%.2f should be %.2f)",
                    unit.name,
                    t,
                    actual_production_cost[t],
                    production_cost
                )
                err_count += 1
            end

            # Verify startup costs
            if abs(actual_startup_cost[t] - startup_cost) > 1.00
                @error @sprintf(
                    "Unit %s has unexpected startup cost at time %d (%.2f should be %.2f)",
                    unit.name,
                    t,
                    actual_startup_cost[t],
                    startup_cost
                )
                err_count += 1
            end
        end
    end

    return err_count
end

function _validate_reserve_and_demand(instance, solution, tol = 0.01)
    err_count = 0
    for t in 1:instance.time
        load_curtail = 0
        fixed_load = sum(b.load[t] for b in instance.buses)
        ps_load = 0
        if length(instance.price_sensitive_loads) > 0
            ps_load = sum(
                solution["Price-sensitive loads (MW)"][ps.name][t] for
                ps in instance.price_sensitive_loads
            )
        end
        production =
            sum(solution["Production (MW)"][g.name][t] for g in instance.units)
        if "Load curtail (MW)" in keys(solution)
            load_curtail = sum(
                solution["Load curtail (MW)"][b.name][t] for
                b in instance.buses
            )
        end
        balance = fixed_load - load_curtail - production + ps_load

        # Verify that production equals demand
        if abs(balance) > tol
            @error @sprintf(
                "Non-zero power balance at time %d (%.2f + %.2f - %.2f - %.2f != 0)",
                t,
                fixed_load,
                ps_load,
                load_curtail,
                production,
            )
            err_count += 1
        end

        # Verify spinning reserves
        reserve =
            sum(solution["Reserve (MW)"][g.name][t] for g in instance.units)
        if reserve < instance.reserves.spinning[t] - tol
            @error @sprintf(
                "Insufficient spinning reserves at time %d (%.2f should be %.2f)",
                t,
                reserve,
                instance.reserves.spinning[t],
            )
            err_count += 1
        end
    end

    return err_count
end
