-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at: http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local log = require 'mc.logging'
local class = require 'mc.class'
local object_pool = require 'object_pool'
local crc8 = require 'mc.crc8'
local utils = require 'mc.utils'
local tasks = require 'mc.orm.tasks'
local sd_bus = require 'sd_bus'
local skynet = require 'skynet'
local skynet_queue = require 'skynet.queue'
local bs = require 'mc.bitstring'
local debounce = require 'mc.debounce.debounce'
local mc_context = require 'mc.context'
local c_timer = skynet.timer
local new_timer = c_timer.new
local stop_timer = c_timer.stop

local context = mc_context.new()
local g_bus = nil
local g_queue = skynet_queue()
local E_OK = nil -- 函数执行成功返回nil
local E_FAILED = '' -- 空错误信息
local FRAME_DATA_LEN <const> = 0x08
local INIT_SLIDE_WIN_LEN <const> = 0x40
local CAN_EXTENDED_FRAME <const> = 4
local CAN_POWER_PROTOCOL <const> = 0x3f
local BIT_M_CNT <const> = 1    -- 发送帧的主设置
local CMD_DOWNLOAD_TRANSEFER <const> = 0xD4    -- 在线加载过程中，数据帧传送
local READ_FAIL_DEBOUNCE_COUNT <const> = 5   -- 防抖次数

local ONE_POWER_CONVERTER_BLACK_BOX_LEN <const> = 34
-- 读取电源砖黑匣子的总帧数
local POWER_CONVERTER_READ_FRAME_CNT <const> = 0xEF
-- 设置下一步要提取的电源砖黑匣子帧编号
local POWER_CONVERTER_WRITE_FRAME_ID <const> = 0xEA
-- 读取电源砖黑匣子第i帧
local POWER_CONVERTER_READ_FRAME_DATA <const> = 0xEB
local RETRY_TIMES <const> = 3
-- 输入功耗变化阈值
local INPUT_POWER_WATTS_THRESHOLD <const> = 3
-- 输入功耗初始值，需要保证功耗刷新为0或以上功耗时，变化阈值大于3w
local INPUT_POWER_WATTS_DEFAULT <const> = -4
-- 电源通讯异常初始值
local COMMUNICATION_STATUS_DEFAULT <const> = -1

local function is_g_bus_not_empty()
    return g_bus and g_bus.bus and g_bus.bus.conn
end

local function get_bus()
    if is_g_bus_not_empty() then
        return g_bus
    end

    g_queue(function ()
        if is_g_bus_not_empty() then
            return g_bus
        end
        g_bus = sd_bus.open_user(true)
    end)
    
    return g_bus
end

local skynet_timer = skynet.timer

local cmds = {}

local DEFAULT_MASK<const> = 0xffffffff
local BLOCK_ACCESS_TYPE<const> = 1
local LOAD_CTRL           = 0xFC
local LOAD_DATA           = 0xFD
local READ_PIN            = 0x97
local READ_POUT           = 0x96

local RET_OK<const> = 0
local RET_ERR<const> = -1

local function sub_frame_data_from_bin(addr, soft_info_from_bin, frame_len, frm_number)
    local data = soft_info_from_bin:sub(frm_number * frame_len + 1, (frm_number + 1) * frame_len)
    local check_buf = table.concat({ 
            string.char(addr),
            string.char(LOAD_DATA),
            string.char((frame_len & 0xff) + 2),
            string.char(frm_number & 0xff),
            string.char(frm_number >> 8),
            data })
    local crc = crc8(check_buf)
    return (check_buf .. string.char(crc)):sub(3, -1)
end

local function chip_write(chip, cmd, data)
    log:debug("power write cmd:0x%x data:%s", cmd, utils.to_hex(data))
    local input = object_pool.new('input_args', cmd, DEFAULT_MASK, BLOCK_ACCESS_TYPE, #data)
    chip:write(input, data)
end

local function chip_read(chip, cmd, len)
    log:debug("int plugins chip read cmd%s   len%s", cmd, len)
    local input = object_pool.new('input_args', cmd, DEFAULT_MASK, BLOCK_ACCESS_TYPE, len, nil)
    return chip:read(input)
end

-- 封装crc8校验，与read_data比较
local function check_data(check_sum, data)
    local crc = crc8(data)
    if crc ~= check_sum then
        log:error('check crc failed, get crc: %d, real crc: %d', check_sum, crc)
        return false
    end
    return true
end

local function ctrl_cmd_read(chip, cmd)
    local value = chip_read(chip, cmd, 3)
    local slot_i2c_addr = chip.driver.address
    local check_buf = string.format('%s%s%s%s',
        string.char(slot_i2c_addr),
        string.char(cmd),
        string.char(slot_i2c_addr | 0x01),
        value:sub(1, #value - 1))
    if value and check_data(value:sub(#value, #value):byte(), check_buf) then
        return string.unpack('H', value:sub(1, 2))
    end
    error(string.format('[plugins][power_mgmt]read commad(0x%x) failed, data:[%s]', cmd, utils.to_hex(value)))
end

function cmds.load_soft_bin_data_by_plugins(chip, addr, frame_len, block_num, wait_time, soft_info_from_bin)
    local resp, data, ok
    for i = 1, block_num do
        data = sub_frame_data_from_bin(addr, soft_info_from_bin, frame_len, i - 1)
        for _ = 1, 3 do
            ok = pcall(function()
                chip_write(chip, LOAD_DATA, data)
            end
            )
            skynet.sleep(wait_time / 10)
            if ok then
                ok, resp = pcall(function()
                    return ctrl_cmd_read(chip, LOAD_CTRL)
                end)
                if ok and resp == 0 then
                    skynet.sleep(1)
                    break
                end
                log:error('Load data frame ok. Get load result failed. Frame %d, resp = %s', i - 1, resp)
            end
            log:error('retry to write to psu chip. Frame %d', i - 1)
        end
    end
end

local function update_input_power_watts_by_pmbus(chip, psu_obj, need_update_values)
    local value, debounced_val = 0, 0
    local ok, raw_word = pcall(function()
        return ctrl_cmd_read(chip, READ_PIN)
    end)
    if not ok then
        -- 1代表通讯失败
        debounced_val = psu_obj.cont_bin:get_debounced_val(1)
        if psu_obj.communication_status ~= debounced_val then
            need_update_values.communication_status = debounced_val
            psu_obj.communication_status = need_update_values.communication_status
            psu_obj.input_power_watts = INPUT_POWER_WATTS_DEFAULT
        end
        return
    end
    -- 0代表通讯成功
    debounced_val = psu_obj.cont_bin:get_debounced_val(0)
    if psu_obj.communication_status ~= debounced_val then
        need_update_values.communication_status = debounced_val
        psu_obj.communication_status = need_update_values.communication_status
    end
    local val = raw_word & 0x7FF
    local bit = ((raw_word >> 11) & 0x1f)
    if raw_word & 0x8000 > 0 then
        bit = (255 - (bit | 0xE0)) + 1
        --  电流较小（0~1 A）时，以A为单位传值会导致精度丢失；
        --  计算电流时，将读数先放大，以10mA为单位传值以保留精度；
        value = val >> bit
    else
        value = val << bit
    end
    if math.abs(value - psu_obj.input_power_watts) > INPUT_POWER_WATTS_THRESHOLD then
        need_update_values.input_power_watts = value
        psu_obj.input_power_watts = value
    end
end

local function update_powerconverter_power_watts_by_pmbus(chip, psu_obj, need_update_values)
    local value, debounced_val = 0, 0
    local ok, raw_word = pcall(function()
        return ctrl_cmd_read(chip, READ_POUT)
    end)
    if not ok then
        -- 1代表通讯失败
        debounced_val = psu_obj.cont_bin:get_debounced_val(1)
        if psu_obj.communication_status ~= debounced_val then
            need_update_values.communication_status = debounced_val
            psu_obj.communication_status = need_update_values.communication_status
            psu_obj.input_power_watts = INPUT_POWER_WATTS_DEFAULT
        end
        return
    end
    -- 0代表通讯成功
    debounced_val = psu_obj.cont_bin:get_debounced_val(0)
    if psu_obj.communication_status ~= debounced_val then
        need_update_values.communication_status = debounced_val
        psu_obj.communication_status = need_update_values.communication_status
    end
    -- 电源功率计算，变量bit代表raw_word高5bit，是指数位；变量val代表raw_word低11bit，是数据位
    local val = raw_word & 0x7FF
    local bit = ((raw_word >> 11) & 0x1f)
    -- raw_word高5bit是指数位，需要判断最高位，分别进行电源公式处理
    if raw_word & 0x8000 > 0 then
        bit = (255 - (bit | 0xE0)) + 1
        --  电流较小（0~1 A）时，以A为单位传值会导致精度丢失；
        --  计算电流时，将读数先放大，以10mA为单位传值以保留精度；
        value = val >> bit
    else
        value = val << bit
    end
    -- 获取电源砖输入功率 = 输出功率 / 转换系数，转换系数 = 0.95
    value = value * 100 // 95
    if math.abs(value - psu_obj.input_power_watts) > INPUT_POWER_WATTS_THRESHOLD then
        need_update_values.input_power_watts = value
        psu_obj.input_power_watts = value
    end
end

local function wait_and_timeout(fetch)
    local co = coroutine.running()
    -- 2秒超时
    local timer = new_timer(2000, function()
        if fetch.result ~= nil then
            return
        end
        skynet.wakeup(co)
    end)

    fetch.cb = function()
        if not timer then
            return
        end
        stop_timer(timer)
        skynet.wakeup(co)
    end

    skynet.wait() --等待当前协程被唤醒

    if fetch.result == nil then
        fetch.timeout = true
        error('request_timeout')
    end
end
 
local function wait_hw_access(fetch)
    if fetch.result == nil then
        wait_and_timeout(fetch)
    end

    if fetch.result == false then
        local msg = fetch.error
        object_pool.recycle('fetch', fetch)
        error(msg)
    end

    local result = fetch.result
    object_pool.recycle('fetch', fetch)
    return result
end

local g_psm_tasks = {}
function cmds.start_power_monitor(chip, ps_id, path, interval)
    for task_id, task in pairs(g_psm_tasks) do
        if task.ps_id == ps_id then
            task:set_timeout_ms(interval * 10)
            log:notice("set [power] read interval %s successfully", interval)
            return task_id
        end
    end
    local psu_obj = {
        ['input_power_watts'] = INPUT_POWER_WATTS_DEFAULT,
        ['communication_status'] = COMMUNICATION_STATUS_DEFAULT,
         -- 读数失败防抖, 连续40次读取失败, 产生告警, 连续40次成功, 解除告警
         cont_bin = debounce[debounce.DEBOUNCED_CONT].new(40, COMMUNICATION_STATUS_DEFAULT)
    }
    local need_update_values = {}
    local task_id = #g_psm_tasks + 1
    local ok, debounced_val
    local task = tasks.get_instance():new_task('power_monitor_task' .. task_id):loop(function()
        ok = pcall(function ()
            local fetch = {}
            chip:plugin_request(fetch, function()
                update_input_power_watts_by_pmbus(chip, psu_obj, need_update_values)
                return true
            end)

            wait_hw_access(fetch)
        end)
        if not ok then
            debounced_val = psu_obj.cont_bin:get_debounced_val(1)
            if psu_obj.communication_status ~= debounced_val then
                need_update_values.communication_status = debounced_val
                psu_obj.communication_status = need_update_values.communication_status
                psu_obj.input_power_watts = INPUT_POWER_WATTS_DEFAULT
            end
        end

        if next(need_update_values) then
            get_bus():signal(path, 'bmc.kepler.power_mgmt', 'UpdateProps', 'a{ss}a{sd}', context, need_update_values)
            need_update_values = {}
        end
    end):set_timeout_ms(interval * 10)

    task.ps_id = ps_id
    g_psm_tasks[task_id] = task
    log:notice('start psu power monitor task, ps_id=%d', ps_id)
    task.on_task_exit:on(function()
        g_psm_tasks[task_id] = nil
        log:notice('psu power monitor task exit, ps_id=%d', ps_id)
    end)
    return task_id
end

local g_powerconverter_tasks = {}
function cmds.start_powerconverter_power_monitor(chip, ps_id, path, interval)
    for task_id, task in pairs(g_powerconverter_tasks) do
        if task.ps_id == ps_id then
            task:set_timeout_ms(interval * 10)
            log:notice("Set [power] read interval %s successfully", interval)
            return task_id
        end
    end
    local psu_obj = {
        ['input_power_watts'] = INPUT_POWER_WATTS_DEFAULT,
        ['communication_status'] = COMMUNICATION_STATUS_DEFAULT,
         -- 读数失败防抖, 连续40次读取失败, 产生告警, 连续5次成功, 解除告警
         cont_bin = debounce[debounce.DEBOUNCED_CONT].new(40, COMMUNICATION_STATUS_DEFAULT)
    }
    local need_update_values = {}
    local task_id = #g_powerconverter_tasks + 1
    local ok, debounced_val
    local task = tasks.get_instance():new_task('powerconverter_power_monitor_task' .. task_id):loop(function()
        ok = pcall(function ()
            local fetch = {}
            chip:plugin_request(fetch, function()
                update_powerconverter_power_watts_by_pmbus(chip, psu_obj, need_update_values)
                return true
            end)

            wait_hw_access(fetch)
        end)
        if not ok then
            debounced_val = psu_obj.cont_bin:get_debounced_val(1)
            if psu_obj.communication_status ~= debounced_val then
                need_update_values.communication_status = debounced_val
                psu_obj.communication_status = need_update_values.communication_status
                psu_obj.input_power_watts = INPUT_POWER_WATTS_DEFAULT
            end
        end

        if next(need_update_values) then
            get_bus():signal(path, 'bmc.kepler.power_mgmt', 'UpdateProps', 'a{ss}a{sd}', context, need_update_values)
            need_update_values = {}
        end
    end):set_timeout_ms(interval * 10)

    task.ps_id = ps_id
    g_powerconverter_tasks[task_id] = task
    log:notice('Start powerconverter power monitor task, ps_id=%d', ps_id)
    task.on_task_exit:on(function()
        g_powerconverter_tasks[task_id] = nil
        log:notice('Powerconverter power monitor task exit, ps_id=%d', ps_id)
    end)
    return task_id
end

function cmds.stop_power_monitor(chip, task_id)
    local task = g_psm_tasks[task_id]
    if task then
        log:notice('stop psu power monitor task, ps_id=%d', task.ps_id)
        task:stop()
    end
end

function cmds.stop_powerconverter_power_monitor(chip, task_id)
    local task = g_powerconverter_tasks[task_id]
    if task then
        log:notice('Stop powerconverter power monitor task, ps_id=%d', task.ps_id)
        task:stop()
    end
end

local power_mgmt_plugins = class()

function power_mgmt_plugins:has_cmd(cmd_name)
    return cmds[cmd_name] ~= nil
end

function power_mgmt_plugins:run_cmd(chip, cmd, ...)
    return g_queue(function (...)
        log:notice('[power_mgmt_plugins] run cmd[%s]', cmd)
        return cmds[cmd](chip, ...)
    end, ...)
end

-- canbus电源升级插件
local canbus_upgrade_frame_info = bs.new([[<<
    cnt:1,
    reserve:6,
    ms:1,
    cmd:8,
    addr:7,
    protocol:6,
    frame_type:3,
    data/string
>>]])

-- canbus升级写接口，不需要读
local function canbus_upgrade_chip_write(chip, canbus_send_data)
    -- write data
    local ok, value = pcall(function()
        local input = object_pool.new('input_args', 0, DEFAULT_MASK, BLOCK_ACCESS_TYPE, #canbus_send_data)
        return chip:write(input, canbus_send_data)
    end)
    if not ok then
        log:notice('canbus_upgrade_chip_write failed, ok %s value %s', ok, value)
        return E_FAILED
    end
    return E_OK
end

-- canbus电源升级，数据帧传送
local function send_download_frame(chip, data, real_send_len, is_continue, frame_id, slot_addr)
    log:info('send_download_frame start')
    local frame_data = data:sub(frame_id * FRAME_DATA_LEN + 1, frame_id * FRAME_DATA_LEN + real_send_len)
    if is_continue == 0 and real_send_len ~= FRAME_DATA_LEN then
        frame_data = frame_data .. string.rep(string.char(0), FRAME_DATA_LEN - real_send_len)
    end
    local ret = canbus_upgrade_chip_write(chip, canbus_upgrade_frame_info:pack{
        cnt = is_continue,
        reserve = slot_addr,
        ms = BIT_M_CNT,
        cmd = CMD_DOWNLOAD_TRANSEFER,
        addr = slot_addr,
        protocol = CAN_POWER_PROTOCOL,
        frame_type = CAN_EXTENDED_FRAME,
        data = frame_data,
    })
    return ret
end

-- canbus电源升级，电源固件滑窗frame帧发送
function cmds.canbus_load_firmware_by_frame(chip, block_id, frame_num, block_data, len, slot_addr)
    for frame_id = 0, frame_num - 1 do
        log:info('canbus_load_firmware_by_frame %dth frame start', frame_id)
        local offset = block_id * INIT_SLIDE_WIN_LEN + frame_id  -- 当前已发送总帧数
        if offset * FRAME_DATA_LEN > len then
            log:error('canbus_load_firmware_by_frame, len is error')
            return E_FAILED
        end
        -- 最后一帧，置结尾标识
        local is_continue  -- 0代表最后一帧，1代表非最后一帧
        if frame_id == frame_num - 1 then
            is_continue = 0
        else
            is_continue = 1
        end
        -- 当文件大小不是8的整数倍，传输剩余的byte， 其他byte初始化为0
        local real_send_len  -- 实际每帧传送的字节数
        if ((offset + 1) * FRAME_DATA_LEN - len) > 0 then
            real_send_len = len - offset * FRAME_DATA_LEN
        else
            real_send_len = FRAME_DATA_LEN
        end
        local ret = send_download_frame(chip, block_data, real_send_len, is_continue, frame_id, slot_addr)
        if ret ~= E_OK then
            log:error('send_download_frame, write %dth frame failed', frame_id)  -- 此处不退出，继续发送
        end
        skynet.sleep(1)  -- 间隔10ms
    end
    return E_OK
end

-- canbus电源升级，电源固件滑窗frame帧发送
function cmds.canbus_load_firmware_by_frame_tpsu(chip, block_id, frame_num, block_data, len, slot_addr)
    for frame_id = 0, frame_num - 1 do
        log:info('canbus_load_firmware_by_frame %dth frame start', frame_id)
        local offset = block_id * INIT_SLIDE_WIN_LEN + frame_id  -- 当前已发送总帧数
        if offset * FRAME_DATA_LEN > len then
            log:error('canbus_load_firmware_by_frame, len is error')
            return E_FAILED
        end
        -- 最后一帧，置结尾标识
        local is_continue  -- 0代表最后一帧，1代表非最后一帧
        if frame_id == frame_num - 1 then
            is_continue = 0
        else
            is_continue = 1
        end
        -- 当文件大小不是8的整数倍，传输剩余的byte， 其他byte初始化为0
        local real_send_len  -- 实际每帧传送的字节数
        if ((offset + 1) * FRAME_DATA_LEN - len) > 0 then
            real_send_len = len - offset * FRAME_DATA_LEN
        else
            real_send_len = FRAME_DATA_LEN
        end
        local ret = send_download_frame(chip, block_data, real_send_len, is_continue, frame_id, slot_addr)
        if ret ~= E_OK then
            log:error('send_download_frame, write %dth frame failed', frame_id)  -- 此处不退出，继续发送
        end
        tasks.sleep_ms(2)
    end
    return E_OK
end

-- chip_read 读取指定长的数据
local function pmbus_chip_read(chip, cmd, len, slot_i2c_addr)
    local err_str = ''
    local value
    local check_buf
    for _ = 1, RETRY_TIMES, 1 do
        value = chip_read(chip, cmd, len)
        check_buf = string.format('%s%s%s%s',
            string.char(slot_i2c_addr),
            string.char(cmd),
            string.char(slot_i2c_addr | 0x01),
            value:sub(1, #value - 1))
        if value and check_data(value:sub(#value, #value):byte(), check_buf) then
            return value
        end
        err_str = string.format('[power_mgmt][pmbus]read commad(0x%02x) failed', cmd)
    end
    error(err_str)
end

local function get_powerconverter_blackbox_frame_cnt(chip, slot_i2c_addr)
    -- 3 crc
    local data = pmbus_chip_read(chip, POWER_CONVERTER_READ_FRAME_CNT, 3, slot_i2c_addr)
    -- 1-2 读两个字节
    return string.unpack('H', data:sub(1, 2))
end

local function write_powerconverter_blackbox_frame_id(chip, data, slot_i2c_addr)
    local check_buf = string.format('%s%s%s', string.char(slot_i2c_addr),
                    string.char(POWER_CONVERTER_WRITE_FRAME_ID), string.pack('H', data))
    local crc = crc8(check_buf)
    data = string.pack('H', data) .. string.pack('B', crc)
    return chip_write(chip, POWER_CONVERTER_WRITE_FRAME_ID, data)
end

local function get_powerconverter_blackbox_frame_data(chip, slot_i2c_addr)
    local data = pmbus_chip_read(chip, POWER_CONVERTER_READ_FRAME_DATA,
                 ONE_POWER_CONVERTER_BLACK_BOX_LEN + 2, slot_i2c_addr)
    -- 1 len, 最后1个字节 crc, 黑匣子信息4到#data-1
    data = data:sub(4, #data - 1)
    return data
end

function cmds.start_powerconverter_blackbox(chip, slot_i2c_addr, ps_id)
    local ok, data, cnt
    local black_box_data = ""
    log:error('start_powerconverter_blackbox, ps_id %s', ps_id)
    ok, cnt = pcall(get_powerconverter_blackbox_frame_cnt, chip, slot_i2c_addr)
    if not ok then
        log:error('get blackbox frame cnt failed, ok %s, cnt %s', ok, cnt)
        return E_FAILED, ""
    end
    -- 帧时间间隔30ms
    skynet.sleep(3)
    log:error('blackbox total frame cnt %s', cnt)
    for i = 0, cnt - 1 do
        ok, data = pcall(write_powerconverter_blackbox_frame_id, chip, i, slot_i2c_addr)
        if not ok then
            log:error('write %x block failed, ok %s, data %s', i, ok, data)
            return E_FAILED, ""
        end
        -- 帧时间间隔30ms
        skynet.sleep(3)
        ok, data = pcall(get_powerconverter_blackbox_frame_data, chip, slot_i2c_addr)
        if not ok then
            log:error('get %x frame data failed, ok %s, data %s', i, ok, data)
            return E_FAILED, ""
        end
        black_box_data = black_box_data .. data
        -- 帧时间间隔30ms
        skynet.sleep(3)
    end
    return E_OK, black_box_data
end

return power_mgmt_plugins