"""
pistochini.py - run simulation using CO2 DCV.
"""

import os
from argparse import ArgumentParser
from dataclasses import dataclass
from contamxpy import cxLib
from dcv_pi_control import DcvPiControl
from dcv_pi_control import PiControlData


@dataclass
class ControlIdxName:
    """
    Contains Index and Name of a control within either `cxLib.outputControls`
    list or `cxLib.inputControls` list.
    """

    index: int
    name: str


# =========================================================================
# USER INPUT - Set control-related GLOBAL variables.
# =========================================================================
#   INDEXes and Names can be obtained by running this script and viewing
# OUTPUT CONTROLS and INPUT CONTROLS lists. These indices are not
# necessarily the same as the control IDs in ContamW. They are indices into
# the corresponding named SET and SPLIT controls in the project file.


# Process Variable: PRJ Control to get CO2 conc
PV_PASS_CTRL = ControlIdxName(1, "CO2box")
# Control Output:   PRJ Control to set Vent rate
CO_SET_CTRL = ControlIdxName(3, "set_Voa")

KP = 0.076  # L/s-ppm
TAU = 480  # 8 min in seconds
DB = 25  # Dead band [ppm]
VRmin = 3.95  # L/s
VRmax = 26.5  # L/s
CO2_SET_POINT = 1000  # ppm
CONTROL_DATA = PiControlData(KP, TAU, DB, VRmin, VRmax)

# ====================================================== check_controls() =====


def check_controls(cxl, test, pv_ctrl, co_ctrl) -> int:
    """
    Check to see if controls are properly defined within the PRJ.
    There should be a named SPLIT/PASS control to provide the CO2
    to this module, i.e., process value (pv) and a named SET control
    for the ventilation rate, i.e., controller output (co).

        Args:
            cxl: `cxLib` instance for specific PRJ
            test: `bool`
                Print list of input/output controls in the PRJ
            pv_ctrl: `CtrlIdxName`
                Index and Name of cxLib.outputControls[] list item
            co_ctrl: `CtrlIdxName`
                Index and Name of cxLib.inputControls[] list item

        :returns: Number of errors found. 0 if `test` is True
        :rtype: int
    """
    input_error = 0
    if test is True:
        # ----- CONTROLS PROPERTIES
        print("----- OUTPUT CONTROLS -----")
        for i, ctrl in zip(range(cxl.nOutputControls), cxl.outputControls):
            ctrl_name = ctrl.name
            print(f"{i+1} {ctrl_name}")
        print("\n")
        print("----- INPUT CONTROLS -----")
        for i, ctrl in zip(range(cxl.nInputControls), cxl.inputControls):
            ctrl_name = ctrl.name
            print(f"{i+1} {ctrl_name}")
        print("\n")
        return input_error

    # Check to see if control indices are set correctly.
    if (
        pv_ctrl.index > cxl.nOutputControls
        or cxl.outputControls[pv_ctrl.index - 1].name != pv_ctrl.name
    ):
        print(f"Incorrect index ({pv_ctrl.index}) for output control: {pv_ctrl.name}")
        input_error += 1
    if (
        co_ctrl.index > cxl.nInputControls
        or cxl.inputControls[co_ctrl.index - 1].name != co_ctrl.name
    ):
        print(f"Incorrect index ({co_ctrl.index}) for input control: {co_ctrl.name}")
        input_error += 1

    if input_error > 0:
        print("Run this script with -t option to determine correct control indices.")

    return input_error


# ========================================= callback_set_initial_values() =====


def callback_set_initial_values(cxl: cxLib):
    """
    Callback function to initialize control values
    - Outdoor air intake
    - Supply and Return airflow rate
    """
    cxl.setInputControlValue(CO_SET_CTRL.index, CONTROL_DATA.co_min)

    print("CONTROL DATA:")
    print(f"  kp = {CONTROL_DATA.kp}\n  tau = {CONTROL_DATA.tau}")
    print(f"  db = {CONTROL_DATA.db}\n  Voa_min = {CONTROL_DATA.co_min}")
    print(f"  Voa_max = {CONTROL_DATA.co_max}\n")


# ================================================================ main() =====


def main():
    """
    This program takes the full name of a PRJ file and runs the simulation from
    beginning to end. It implements a PI control DCV algorithm.
    """
    # ----- Manage option parser
    parser = ArgumentParser(
        prog="Pistochini_2",
        description="Implements a PI DCV control algorithm for a single zone",
    )
    parser.add_argument("filename")
    parser.add_argument(
        "-v",
        "--verbose",
        type=int,
        choices=range(0, 3),
        help="define verbose output level: 0=Min, 1=Medium, 2=Maximum.",
    )
    parser.add_argument(
        "-t",
        "--test",
        action="store_true",
        help="do not run simulation, but output lists of available controls.",
    )
    parser.set_defaults(verbose=0, test=False)
    args = parser.parse_args()

    # ----- Process command line options -v
    verbose = args.verbose
    test = args.test

    prj_file_path = args.filename

    if not os.path.exists(prj_file_path):
        print(f"ERROR: PRJ file not found: {prj_file_path}")
        return

    msg_cmd = args
    print(msg_cmd, "\n")

    if verbose > 1:
        print(f"cxLib attributes =>\n{chr(10).join(map(str, dir(cxLib)))}\n")

    # ----- Initialize contamx-lib object w/ wp_mode and cb_option.
    #   wp_mode = 0 => use wind pressure profiles, WTH and CTM files or
    # associated API calls.
    #   cb_option = True => set callback function to get PRJ INFO
    # from the ContamXState.
    my_prj = cxLib(prj_file_path, 0, True, callback_set_initial_values)
    my_prj.setVerbosity(verbose)

    # ----- Provide version information
    print(f"Version = {my_prj.getVersion()}.\n")

    # ----- Setup Simulation for PRJ
    sim_not_ok = my_prj.setupSimulation(1)
    if sim_not_ok > 0:
        print(f"ABORT - sim_not_ok Returned by setupSimulation() = {sim_not_ok}")
        print(" => invalid simulation parameters for co-simulation.")
        my_prj.endSimulation()
        return

    err_count = check_controls(my_prj, test, PV_PASS_CTRL, CO_SET_CTRL)
    if err_count > 0 or test is True:
        return

    # =========================================================================
    # USER INPUT - Initialize controller class.
    # =========================================================================

    oa_controller = DcvPiControl(CONTROL_DATA)

    # ----- Get simulation run info
    start_day = my_prj.getSimStartDate()
    end_day = my_prj.getSimEndDate()
    start_time = my_prj.getSimStartTime()
    end_time = my_prj.getSimEndTime()
    dt = my_prj.getSimTimeStep()

    sim_begin_sec = (start_day - 1) * 86400 + start_time
    sim_end_sec = (end_day - 1) * 86400 + end_time

    # ----- Calculate the simulation duration in seconds and total time steps
    sim_begin_sec = (start_day - 1) * 86400 + start_time
    sim_end_sec = (end_day - 1) * 86400 + end_time
    if sim_begin_sec <= sim_end_sec:
        sim_duration_sec = sim_end_sec - sim_begin_sec
    else:
        sim_duration_sec = 365 * 86400 - sim_end_sec + sim_begin_sec
    num_time_steps = 0
    if sim_duration_sec != 0:
        num_time_steps = int(sim_duration_sec / dt)
        print(f"PRJ settings => Transient simulation w/ {num_time_steps} time steps.")
    else:
        print("PRJ settings => Steady state simulation.")

    # ----- Get the current date/time after initial steady state simulation
    current_date = my_prj.getCurrentDayOfYear()
    current_time = my_prj.getCurrentTimeInSec()
    if verbose > 0:
        print(f"Sim days = {start_day}:{end_day}")
        print(f"Sim times = {start_time}:{end_time}")
        print(f"Sim time step = {dt}")
        print(f"Number of steps = {num_time_steps}")

    # ----- Initialize result files

    # ----- Output initial results.
    current_date = my_prj.getCurrentDayOfYear()
    current_time = my_prj.getCurrentTimeInSec()
    co2_box = my_prj.getOutputControlValue(PV_PASS_CTRL.index)
    print("day\ttime\tCO2box\tVoaSet\tPMbox")
    print(f"{current_date}\t{current_time}\t{co2_box}\t{CONTROL_DATA.co_min}")

    # =========================================================================
    # Run Simulation
    # =========================================================================
    for i in range(num_time_steps):

        # =====================================================================
        # Tasks to perform BEFORE current time step.
        # =====================================================================
        # Implement control algorithm - PI with anti-windup [Pistochini 2023]

        voa_set = oa_controller.set_co(co2_box, CO2_SET_POINT, dt)
        my_prj.setInputControlValue(CO_SET_CTRL.index, voa_set)

        # =====================================================================
        # Run next time step.
        # =====================================================================
        my_prj.doSimStep(1)

        # =====================================================================
        # Tasks to perform AFTER current time step.
        # =====================================================================
        current_date = my_prj.getCurrentDayOfYear()
        current_time = my_prj.getCurrentTimeInSec()
        co2_box = my_prj.getOutputControlValue(PV_PASS_CTRL.index)
        print(f"{current_date}\t{current_time}\t{co2_box}\t{voa_set}")

    my_prj.endSimulation()


# --- End main() ---#


if __name__ == "__main__":
    main()
