--
-- Process Control App
--

local types         = require("scada-common.types")
local util          = require("scada-common.util")

local iocontrol     = require("pocket.iocontrol")
local pocket        = require("pocket.pocket")
local process       = require("pocket.process")

local style         = require("pocket.ui.style")

local core          = require("graphics.core")

local Div           = require("graphics.elements.Div")
local MultiPane     = require("graphics.elements.MultiPane")
local Rectangle     = require("graphics.elements.Rectangle")
local TextBox       = require("graphics.elements.TextBox")

local WaitingAnim   = require("graphics.elements.animations.Waiting")

local HazardButton  = require("graphics.elements.controls.HazardButton")
local RadioButton   = require("graphics.elements.controls.RadioButton")

local NumberField   = require("graphics.elements.form.NumberField")

local IconIndicator = require("graphics.elements.indicators.IconIndicator")

local ALIGN  = core.ALIGN
local cpair  = core.cpair
local border = core.border

local APP_ID = pocket.APP_ID

local label_fg_bg     = style.label
local text_fg         = style.text_fg

local field_fg_bg     = style.field
local field_dis_fg_bg = style.field_disable

local red_ind_s       = style.icon_states.red_ind_s
local yel_ind_s       = style.icon_states.yel_ind_s
local grn_ind_s       = style.icon_states.grn_ind_s
local wht_ind_s       = style.icon_states.wht_ind_s

local hzd_fg_bg       = style.hzd_fg_bg
local dis_colors      = cpair(colors.white, colors.lightGray)

-- new process control page view
---@param root Container parent
local function new_view(root)
    local db = iocontrol.get_db()

    local frame = Div{parent=root,x=1,y=1}

    local app = db.nav.register_app(APP_ID.PROCESS, frame, nil, false, true)

    local load_div = Div{parent=frame,x=1,y=1}
    local main = Div{parent=frame,x=1,y=1}

    TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
    WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.purple,colors._INHERIT)}

    local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}

    app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })

    local page_div = nil ---@type Div|nil

    -- load the app (create the elements)
    local function load()
        local f_ps = db.facility.ps

        page_div = Div{parent=main,y=2,width=main.get_width()}

        local panes = {} ---@type Div[]

        -- create all page divs
        for _ = 1, db.facility.num_units + 3 do
            local div = Div{parent=page_div}
            table.insert(panes, div)
        end

        local last_update = 0
        -- refresh data callback, every 500ms it will re-send the query
        local function update()
            if util.time_ms() - last_update >= 500 then
                db.api.get_proc()
                last_update = util.time_ms()
            end
        end

        --#region unit settings/status

        local rate_limits = {}  ---@type NumberField[]

        for i = 1, db.facility.num_units do
            local u_pane = panes[i]
            local u_div = Div{parent=u_pane,x=2,width=main.get_width()-2}
            local unit = db.units[i]
            local u_ps = unit.unit_ps

            local u_page = app.new_page(nil, i)
            u_page.tasks = { update }

            TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,alignment=ALIGN.CENTER}

            TextBox{parent=u_div,y=3,text="Auto Rate Limit",fg_bg=label_fg_bg}
            rate_limits[i] = NumberField{parent=u_div,x=1,y=4,width=16,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
            TextBox{parent=u_div,x=18,y=4,text="mB/t",width=4,fg_bg=label_fg_bg}

            rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
            rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)

            local ready    = IconIndicator{parent=u_div,y=6,label="Auto Ready",states=grn_ind_s}
            local a_stb    = IconIndicator{parent=u_div,label="Auto Standby",states=wht_ind_s}
            local degraded = IconIndicator{parent=u_div,label="Unit Degraded",states=red_ind_s}

            ready.register(u_ps, "U_AutoReady", ready.update)
            degraded.register(u_ps, "U_AutoDegraded", degraded.update)

            -- update standby indicator
            a_stb.register(u_ps, "status", function (active)
                a_stb.update(unit.annunciator.AutoControl and (not active))
            end)
            a_stb.register(u_ps, "AutoControl", function (auto_active)
                if auto_active then
                    a_stb.update(unit.reactor_data.mek_status.status == false)
                else a_stb.update(false) end
            end)

            local function _set_group(value) process.set_group(i, value - 1) end

            local group = RadioButton{parent=u_div,y=10,options=types.AUTO_GROUP_NAMES,callback=_set_group,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}

            -- can't change group if auto is engaged regardless of if this unit is part of auto control
            group.register(f_ps, "auto_active", function (auto_active)
                if auto_active then group.disable() else group.enable() end
            end)

            group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)

            TextBox{parent=u_div,y=16,text="Assigned Group",fg_bg=style.label}
            local auto_grp = TextBox{parent=u_div,text="Manual",width=11,fg_bg=text_fg}

            auto_grp.register(u_ps, "auto_group", auto_grp.set_value)

            util.nop()
        end

        --#endregion

        --#region process control options page

        local o_pane = panes[db.facility.num_units + 2]
        local o_div = Div{parent=o_pane,x=2,width=main.get_width()-2}

        local opt_page = app.new_page(nil, db.facility.num_units + 2)
        opt_page.tasks = { update }

        TextBox{parent=o_div,y=1,text="Process Options",alignment=ALIGN.CENTER}

        local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
        local mode = RadioButton{parent=o_div,x=1,y=3,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.lightGray,colors.gray),select_color=colors.purple,dis_fg_bg=style.btn_disable}

        mode.register(f_ps, "process_mode", mode.set_value)

        TextBox{parent=o_div,y=9,text="Burn Rate Target",fg_bg=label_fg_bg}
        local b_target = NumberField{parent=o_div,x=1,y=10,width=15,default=0.01,min=0.01,max_frac_digits=2,max_chars=8,allow_decimal=true,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
        TextBox{parent=o_div,x=17,y=10,text="mB/t",fg_bg=label_fg_bg}

        TextBox{parent=o_div,y=12,text="Charge Level Target",fg_bg=label_fg_bg}
        local c_target = NumberField{parent=o_div,x=1,y=13,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
        TextBox{parent=o_div,x=17,y=13,text="M"..db.energy_label,fg_bg=label_fg_bg}

        TextBox{parent=o_div,y=15,text="Generation Target",fg_bg=label_fg_bg}
        local g_target = NumberField{parent=o_div,x=1,y=16,width=15,default=0,min=0,max_chars=16,align_right=true,fg_bg=field_fg_bg,dis_fg_bg=field_dis_fg_bg}
        TextBox{parent=o_div,x=17,y=16,text="k"..db.energy_label.."/t",fg_bg=label_fg_bg}

        b_target.register(f_ps, "process_burn_target", b_target.set_value)
        c_target.register(f_ps, "process_charge_target", c_target.set_value)
        g_target.register(f_ps, "process_gen_target", g_target.set_value)

        --#endregion

        --#region process control page

        local c_pane = panes[db.facility.num_units + 1]
        local c_div = Div{parent=c_pane,x=2,width=main.get_width()-2}

        local proc_ctrl = app.new_page(nil, db.facility.num_units + 1)
        proc_ctrl.tasks = { update }

        TextBox{parent=c_div,y=1,text="Process Control",alignment=ALIGN.CENTER}

        local u_stat      = Rectangle{parent=c_div,border=border(1,colors.gray,true),thin=true,width=21,height=5,x=1,y=3,fg_bg=cpair(colors.black,colors.lightGray)}
        local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",alignment=ALIGN.CENTER}
        local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",height=2,alignment=ALIGN.CENTER,trim_whitespace=true,fg_bg=cpair(colors.gray,colors.lightGray)}

        stat_line_1.register(f_ps, "status_line_1", stat_line_1.set_value)
        stat_line_2.register(f_ps, "status_line_2", stat_line_2.set_value)

        local function _start_auto()
            local limits = {}
            for i = 1, #rate_limits do limits[i] = rate_limits[i].get_numeric() end

            process.process_start(mode.get_value(), b_target.get_numeric(), db.energy_convert_to_fe(c_target.get_numeric()),
                                  db.energy_convert_to_fe(g_target.get_numeric()), limits)
        end

        local start = HazardButton{parent=c_div,x=2,y=9,text="START",accent=colors.lightBlue,callback=_start_auto,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}
        local stop  = HazardButton{parent=c_div,x=13,y=9,text="STOP",accent=colors.red,callback=process.process_stop,timeout=3,fg_bg=hzd_fg_bg,dis_colors=dis_colors}

        db.facility.start_ack = start.on_response
        db.facility.stop_ack = stop.on_response

        start.register(f_ps, "auto_ready", function (ready)
            if ready and (not db.facility.auto_active) then start.enable() else start.disable() end
        end)

        local auto_ready = IconIndicator{parent=c_div,y=14,label="Units Ready",states=grn_ind_s}
        local auto_act   = IconIndicator{parent=c_div,label="Process Active",states=grn_ind_s}
        local auto_ramp  = IconIndicator{parent=c_div,label="Process Ramping",states=wht_ind_s}
        local auto_sat   = IconIndicator{parent=c_div,label="Min/Max Burn Rate",states=yel_ind_s}

        auto_ready.register(f_ps, "auto_ready", auto_ready.update)
        auto_act.register(f_ps, "auto_active", auto_act.update)
        auto_ramp.register(f_ps, "auto_ramping", auto_ramp.update)
        auto_sat.register(f_ps, "auto_saturated", auto_sat.update)

        -- REGISTER_NOTE: for optimization/brevity, due to not deleting anything but the whole element tree
        -- when it comes to unloading the process app, child elements will not directly be registered here
        -- (preventing garbage collection until the parent 'page_div' is deleted)
        page_div.register(f_ps, "auto_active", function (active)
            if active then
                b_target.disable()
                c_target.disable()
                g_target.disable()

                mode.disable()
                start.disable()

                for i = 1, #rate_limits do rate_limits[i].disable() end
            else
                b_target.enable()
                c_target.enable()
                g_target.enable()

                mode.enable()
                if db.facility.auto_ready then start.enable() end

                for i = 1, #rate_limits do rate_limits[i].enable() end
            end
        end)

        --#endregion

        --#region auto-SCRAM annunciator page

        local a_pane = panes[db.facility.num_units + 3]
        local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}

        local annunc_page = app.new_page(nil, db.facility.num_units + 3)
        annunc_page.tasks = { update }

        TextBox{parent=a_div,y=1,text="Automatic SCRAM",alignment=ALIGN.CENTER}

        local auto_scram = IconIndicator{parent=a_div,y=3,label="Automatic SCRAM",states=red_ind_s}

        TextBox{parent=a_div,y=5,text="Induction Matrix",fg_bg=label_fg_bg}
        local matrix_flt  = IconIndicator{parent=a_div,label="Matrix Fault",states=yel_ind_s}
        local matrix_fill = IconIndicator{parent=a_div,label="Charge High",states=red_ind_s}

        TextBox{parent=a_div,y=9,text="Assigned Units",fg_bg=label_fg_bg}
        local unit_crit = IconIndicator{parent=a_div,label="Critical Alarm",states=red_ind_s}

        TextBox{parent=a_div,y=12,text="Facility",fg_bg=label_fg_bg}
        local fac_rad_h = IconIndicator{parent=a_div,label="Radiation High",states=red_ind_s}

        TextBox{parent=a_div,y=15,text="Generation Rate Mode",fg_bg=label_fg_bg}
        local gen_fault = IconIndicator{parent=a_div,label="Control Fault",states=yel_ind_s}

        auto_scram.register(f_ps, "auto_scram", auto_scram.update)
        matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
        matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
        unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
        fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
        gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)

        --#endregion

        -- setup multipane
        local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
        app.set_root_pane(u_pane)

        -- setup sidebar

        local list = {
            { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
            { label = " \x17 ", color = core.cpair(colors.black, colors.purple), callback = proc_ctrl.nav_to },
            { label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = annunc_page.nav_to },
            { label = "OPT", color = core.cpair(colors.black, colors.yellow), callback = opt_page.nav_to }
        }

        for i = 1, db.facility.num_units do
            table.insert(list, { label = "U-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = function () app.switcher(i) end })
        end

        app.set_sidebar(list)

        -- done, show the app
        proc_ctrl.nav_to()
        load_pane.set_value(2)
    end

    -- delete the elements and switch back to the loading screen
    local function unload()
        if page_div then
            page_div.delete()
            page_div = nil
        end

        app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
        app.delete_pages()

        -- show loading screen
        load_pane.set_value(1)
    end

    app.set_load(load)
    app.set_unload(unload)

    return main
end

return new_view
