-- 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 class = require 'mc.class'
local log = require 'mc.logging'
local singleton = require 'mc.singleton'
local c_object_manage = require 'mc.orm.object_manage'
local c_fan_object = require 'fan_object'
local c_fan_type_object = require 'fan_type_object'
local c_fans_object = require 'fans_object'
local c_tasks = require 'mc.orm.tasks'
local ipmi = require 'ipmi'
local comp_code = ipmi.types.Cc
local _, skynet = pcall(require, 'skynet')
local client = require 'thermal_mgmt.client'
local basic_cooling_config = require 'basic_cooling_config'
local c_fan_group_object = require 'fan_group_object'
local c_fan_group_policy = require 'fan_group_policy_object'
local fans_config = require 'basic_cooling.cooling_config.fans_config'
local props = require 'basic_cooling.define.cooling_properties'

local fan_service = class()

local MAX_PWM<const> = 100
local MIN_PWM<const> = 10
local RET_ERR<const> = 1
local RET_OK<const>  = 0

function fan_service:ctor(database)
    self.db = database
    self.fan_board = {}
    self.listener = {}
end

local function is_valid_pwm(pwm)
    return pwm and pwm >= MIN_PWM and pwm <= MAX_PWM
end

function fan_service:set_fan_pwm(system_id, fan_id, pwm_percent)
    -- 为防止影响风扇型号识别的过程，用该标志位来做互斥
    -- 当前风扇识别失败情况下不允许设置
    local fan_obj = c_fan_object.collection:find({SystemId = system_id, FanId = fan_id})
    if not is_valid_pwm(pwm_percent) then
        log:debug('invalid target pwm: %s', pwm_percent)
        return RET_ERR
    end
    if not fan_obj then
        log:debug('cannot find fan: system_id %d, fan_id %d', system_id, fan_id)
        return RET_ERR
    end
    if fan_obj:get_present() == 0 then
        log:debug('unpresent fan: system_id %d, fan_id %d', system_id, fan_id)
        return RET_ERR
    end
    if not fan_obj:configurable() then
        log:debug('invalid fan%d identify status', fan_id)
        return RET_ERR
    end
    fan_obj:set_fan_hardware_pwm(pwm_percent)
    return RET_OK
end

function fan_service:fan_group_speed_adjust(l_pwm)
    -- 手动转速下发不做转速调整
    if l_pwm[0] ~= 1 then
        log:debug("Start fan group speed adjust.")
        -- 分区调速转速修正
        basic_cooling_config.collection:fold(function (_, basic_cooling_config_obj)
            l_pwm = basic_cooling_config_obj:fan_group_speed_adjust(l_pwm)
            basic_cooling_config_obj:set_psu_fan(l_pwm)
        end)

        -- 风扇分组转速差
        c_fan_group_policy.collection:fold(function (_, fan_group_policy_obj)
            l_pwm = fan_group_policy_obj:fan_group_speed_adjust(l_pwm)
        end)
    end
    c_fan_object.collection:fold(function (_, obj)
        if obj.RearStatus ~= 0 or obj.FrontStatus ~= 0 then
            l_pwm[obj.FanId] = math.floor((obj.ExpectedPWM / obj:get_max_supported_pwm() * 100) + 0.5)
        end
    end)
    return l_pwm
end

function fan_service:set_fans_pwm(l_pwm)
    local task_count = 0
    local co = coroutine.running()
    local resp = 0
    local create_task = function (cb)
        local task = c_tasks.get_instance():next_tick(cb)
        task_count = task_count + 1
        task.on_task_exit:on(function ()
            task_count = task_count - 1
            if task_count == 0 then
                -- 所有任务处理完成，唤醒当前任务
                skynet.wakeup(co)
            end
        end)
    end

    l_pwm = self:fan_group_speed_adjust(l_pwm)

    c_fans_object.collection:fold(function (_, obj)
        for index, fan_obj in ipairs(obj.fan_collection) do
            obj.l_pwm[index] = l_pwm[fan_obj.FanId]
            l_pwm[fan_obj.FanId] = nil
        end
        create_task(function ()
            resp = resp | obj:set_pwm(obj.l_pwm)
        end)
    end)
    for fan_id, pwm_percent in pairs(l_pwm) do
        -- 对于风扇id跳变显示的机型，转速下发时候对应id下pwm填充为0
        if not is_valid_pwm(pwm_percent) then
            goto continue
        end
        -- 不支持批量设置转速的风扇，手动逐个下发
        local fan_obj = c_fan_object.collection:find({ FanId = fan_id })
        if fan_obj and fan_obj:get_present() == 1 and fan_obj:is_identified() then
            create_task(function ()
                log:debug('Set fan(%s) hardware pwm to %s', fan_id, pwm_percent)
                fan_obj:set_fan_hardware_pwm(pwm_percent)
            end)
        end
        ::continue::
    end
    skynet.wait(co)
    return resp
end

function fan_service:on_reboot_prepare()
    log:notice('Start to reboot prepare')
    local reset_speed_table = {} -- key为风扇id，value为重启转速
    c_fan_group_object.collection:fold(function (_,fan_group_obj)
        for _, fan_id in pairs(fan_group_obj.FanSlots) do
            reset_speed_table[fan_id] = fan_group_obj.ResetSpeedPercent
        end
    end)

    c_fan_object.collection:fold(function(_, fan_obj)
        if fan_obj:get_present() == 1 then
            if reset_speed_table[fan_obj.FanId] then
                fan_obj:set_fan_hardware_pwm(reset_speed_table[fan_obj.FanId])
            else  --复位配置表中查询不到转速信息，则默认下发最大转速
                fan_obj:set_fan_hardware_pwm(MAX_PWM)
            end
        end
    end)
end

function fan_service:on_reboot_action()
    log:notice('start to reboot action, stop all fan tasks')
    c_fan_object.collection:fold(function(_, obj)
        obj:stop_tasks()
    end)
end

function fan_service:clear_persist_fan_info(position)
    local tbl = self.db.FanInfo
    if self.db == nil or tbl == nil then
        return
    end
    -- 删除数据库中当前已不存在的风扇槽位
    for _, record in pairs(self.db:select(tbl):where(tbl.FanPosition:eq(position)):all()) do
        if not c_fan_object.collection:find(function(obj)
            -- persist_fan_id不是db表项，需要用function匹配
            return obj.SystemId == record.SystemId and
                obj.persist_fan_id == record.FanId and
                obj:get_position() == record.FanPosition
        end) then
            log:error('fan(id:%s, position:%s) slot not existed, removed from db', record.FanId, record.FanPosition)
            record:delete()
        end
    end
end

local function dump_table_info(fp_w, depth, key, value)
    local dump_msg = string.format('%s| %s:', string.rep("-", 2 * depth), key)
    if depth == 0 then
        local info_str = string.format('%s\n', dump_msg)
        fp_w:write(info_str)
        for _, prop_name in pairs(value:get_all_prop_names()) do
            dump_table_info(fp_w, depth + 1, prop_name, value[prop_name])
        end
        return
    end
    if depth > 3 then
        return
    end
    local value_type = type(value)
    local value_str = ''
    if value_type ~= 'table' then
        if value_type == 'string' or value_type == 'number' or value_type == 'boolean' then
            value_str = tostring(value)
            local info_str = string.format('%s       %s\n', dump_msg, value_str)
            fp_w:write(info_str)
        end
    else
        if value['removed_handles'] ~= nil then
            return
        end
        local info_str = string.format('%s\n', dump_msg)
        fp_w:write(info_str)
        for sub_key, sub_value in pairs(value) do
            dump_table_info(fp_w, depth + 1, sub_key, sub_value)
        end
    end
end

function fan_service:dump_obj_infos(fp_w)
    fp_w:write('\nFan:')
    c_fan_object.collection:fold(function(_, obj)
        dump_table_info(fp_w, 0, obj.ObjectName, obj)
        dump_table_info(fp_w, 1, 'last_present', obj.last_present)
        dump_table_info(fp_w, 1, 'model_source', obj.model_source)
        dump_table_info(fp_w, 1, 'default_model', obj.default_model)
        dump_table_info(fp_w, 1, 'fan_identified', obj.fan_identified)
        dump_table_info(fp_w, 1, 'mcu_upgrading', obj.mcu_upgrading)
        dump_table_info(fp_w, 1, 'status_record', obj.status_record)
        dump_table_info(fp_w, 1, 'status_error', obj.status_error)
        dump_table_info(fp_w, 1, 'persist_fan_id', obj.persist_fan_id)
        skynet.sleep(150)
    end)
    log:debug("dump fan object finished.")
    fp_w:write('\nFanType:')
    c_fan_type_object.collection:fold(function(_, obj)
        dump_table_info(fp_w, 0, obj.ObjectName, obj)
        dump_table_info(fp_w, 1, 'fan_num', obj.fan_num)
        skynet.sleep(150)
    end)
    log:debug("dump fan type object finished.")
    fp_w:write('\n\n\n')
end

function fan_service.get_dump_pwm_by_fan_obj(obj)
    local pwm = obj.HardwarePWM
    if obj.PowerGood == 0 and not obj.standbyfan then
        pwm = 0
        log:notice('Get fan(id: %s) pwm success, hardware pwm: %s, pwm: %s',
            obj.FanId, obj.HardwarePWM, pwm)
    else
        log:notice('Get fan(id: %s) pwm success, power good: %s, standby: %s, pwm: %s',
            obj.FanId, obj.PowerGood, obj.standbyfan, pwm)
    end
    return pwm
end

function fan_service:dump_fan_info(fp_w)
    -- 每条日志的格式
    local format_data =
    '%-24s | %-17s | %-8s | %-18s | %-18s | %-16s  |  %-16s  |  %-16s  |  %-16s|  %-16s  |  %-16s\n'
    local presence_map = { [0] = 'absent', [1] = 'present' }
    local title = string.format(format_data,
        'Fan', 'Presence', 'PWM', 'Speed(RPM)', 'Maximum rpm range', 'Model', 'PartNum', 'DeviceName', 'Location',
    'CtrlMode', 'SmartCoolingMode')
    fp_w:write(title)
    local dump_msg = {}
    local fans_config_obj = fans_config.get_instance():get_obj()
    local ctrl_mode, smart_cooling_mode = 'N/A'
    if fans_config_obj then
        smart_cooling_mode = fans_config_obj[props.SMART_COOLING_MODE]
        ctrl_mode = fans_config_obj[props.CTL_MODE]
        log:notice("Get fans config obj success.")
    end
    c_fan_object.collection:fold(function(_, obj)
        if obj:get_present() == 0 then
            -- 不在位显示NA
            dump_msg[obj.FanId] = string.format(format_data,
                string.format('Fan Module%u', obj.FanId), 'absent', 'N/A', 'N/A', 'N/A', 'N/A', 'N/A', 'N/A', 'N/A',
            'N/A', 'N/A')
            return
        end
        local pwm = self.get_dump_pwm_by_fan_obj(obj)
        if obj.IsTwins then
            dump_msg[obj.FanId] = string.format(format_data,
                string.format('Fan Module%u Front/Rear', obj.FanId),
                string.format('%-8s/%-8s', presence_map[obj.FrontPresence], presence_map[obj.RearPresence]),
                pwm, string.format('%-6u/%-11u', obj.FrontSpeed, obj.RearSpeed),
                string.format('%-6u/%-11u', obj.FrontMaxSpeed, obj.RearMaxSpeed),
                obj.Model, obj.PartNumber, string.format('Fan%u', obj.FanId), obj.Position, ctrl_mode,
                smart_cooling_mode)
        else
            dump_msg[obj.FanId] = string.format(format_data, string.format('Fan Module%u', obj.FanId),
                presence_map[obj.RearPresence], pwm, obj.RearSpeed, obj.RearMaxSpeed,
                obj.Model, obj.PartNumber, string.format('Fan%u', obj.FanId), obj.Position, ctrl_mode,
                smart_cooling_mode)
        skynet.sleep(150)
        end
    end)
    -- 存在id不连续的场景，这里不能用ipairs
    for _, data in pairs(dump_msg) do
        fp_w:write(data)
    end
end

function fan_service:get_fan_speed(fan_id, isRearFan)
    local fan_obj = c_fan_object.collection:find({FanId = fan_id})
    if fan_obj ~= nil then
        if fan_obj.RearPresence == 1 and isRearFan == 1 then
            return fan_obj.RearSpeed, comp_code.Success
        end
        if fan_obj.FrontPresence == 1 and isRearFan ~= 1 then
            return self.IsTwins and fan_obj.FrontSpeed or fan_obj.RearSpeed, comp_code.Success
        end
        return 0, comp_code.CommandNotAvailable
    end
    return 0, comp_code.InvalidCommand
end

local function get_max_match_fan_type()
    local max_identified_fan_num = {}
    local identified_fan_type = {}
    local max_identified_fan_num_index = {}
    c_fan_type_object.collection:fold(function(_, obj)
        local position = obj:get_position()
        if not max_identified_fan_num[position] or obj.fan_num > max_identified_fan_num[position] or
            -- 数量相同时，取index较小的类型
            (max_identified_fan_num[position] == obj.fan_num and
            max_identified_fan_num_index[position] > obj.Index) then
            max_identified_fan_num[position] = obj.fan_num
            identified_fan_type[position] = obj.Name
            max_identified_fan_num_index[position] = obj.Index
        end
    end)
    return identified_fan_type, max_identified_fan_num
end

local function check_fan_identified_info()
    local identified_fan_type, max_identified_fan_num = get_max_match_fan_type()
    if not identified_fan_type or not next(identified_fan_type) or
        not max_identified_fan_num or not next(max_identified_fan_num) then
        return
    end
    c_fan_object.collection:fold(function(_, obj)
        if obj:get_present() == 1 and max_identified_fan_num[obj:get_position()] ~= 0 then
            obj:set_default_fan_model(identified_fan_type[obj:get_position()])
        end
    end)
end

function fan_service:monitor_fan_identified()
    while true do
        check_fan_identified_info()
        c_tasks.sleep_ms(5000)
    end
end

function fan_service:fan_board_listenning(obj, position)
    self.listener[#self.listener + 1] = client:OnUnitPropertiesChanged(function(prop_value, path, interface)
        if not string.find(path, "FanBoard") then
            return
        end
        local fan_objs = c_fan_object.collection:fetch_by_position(position)
        for _, fan_obj in pairs(fan_objs) do
            fan_obj:fan_board_changed_handle(interface, prop_value)
        end
    end)
end

function fan_service:start()
    c_tasks.get_instance():next_tick(self.monitor_fan_identified, self)
end

return singleton(fan_service)
