-- 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 c_acu = require 'unit_manager.class.unit.acu.acu'
local log = require 'mc.logging'
local smc = require 'protocol.smc'
local i3c = require 'protocol.i3c'
local bs = require 'mc.bitstring'
local file_sec = require 'utils.file'
local cmn = require 'common'
local utils = require 'mc.utils'
local utils_core = require 'utils.core'
local vos = require 'utils.vos'

---@class NpuBoard: ACU @NPU板
local c_npu_board = class(c_acu)

local RET_ERR<const> = -1
local SMC_MCU_DUMP_ADDRESS_LEN <const> = 4
local SMC_MCU_DUMP_FILE_LEN <const> = 240 -- NPU模组MCU每次获取240字节的数据
local SMC_MCU_DUMP_DATA_LEN <const> = 244 -- 每帧由4字节的偏移和240字节的数据组成
local GET_DUMP_LEN_CMD <const> = 0x00004100
local SET_DUMP_ADDRESS_CMD <const> = 0x00004600
local GET_DUMP_DATA_CMD <const> = 0x00004900

local I3C_BUS_TYPE = 2
local I3C_MCU_DUMP_FILE_LEN<const> = 0x7f4  -- I3C每次获取2k-12的数据
local I3C_MCU_DUMP_DATA_LEN<const> = 0x7f8  -- 每帧由4字节的偏移和2k-12字节的数据组成

local response_data_bs <const> = bs.new([[<<
    offset:32,
    data/string
>>]])

local i3c_log_type_list = {
    ['npu_uart_log'] = 4,
    ['npu_log'] = 5
}

local smc_log_type_list = {
    ['maintaince_log'] = 1,
    ['operate_log'] = 2,
    ['error_log'] = 3,
    ['npu_uart_log'] = 4
}

local log_type_lists = {
    [0] = smc_log_type_list,
    [1] = smc_log_type_list,
    [2] = i3c_log_type_list
}

local i3c_log_max_len = {
    ['npu_uart_log'] = 0x200000,   -- npu日志最大2mb
    ['npu_log'] = 0x200000         -- 串口录音最大2mb
}

local smc_log_max_len = {
    ['maintaince_log'] = 0x19200, -- 0x18C00的flash+0x600的ram
    ['operate_log'] = 0x19000, -- 0x18C00的flash+0x400的ram
    ['error_log'] = 0x32000, -- 0x31800的flash+0x800的ram
    ['npu_uart_log'] = 0x60400 -- 0x60000的flash+0x400的ram
}

local log_max_lens = {
    [0] = smc_log_max_len,
    [1] = smc_log_max_len,
    [2] = i3c_log_max_len
}

local CHANNEL_TYPE = {
    [0] = smc,
    [1] = smc,
    [2] = i3c
}

function c_npu_board:task_update()
    self:task_update_mcu_version()
end

function c_npu_board:update_npuboard_time()
    local chip = self:get_prop('RefSMCChip')
    -- func=0x10, command=0x08, ms=1, rw=0, param=1, data_len=4
    local SET_TIME_CMD <const> = 0x40002201
    local data = string.pack('L', os.time()):sub(1,4)
    local ok = pcall(smc.chip_blkwrite, chip, SET_TIME_CMD, data)
    if not ok then
        log:error('SMC write (update npuboard time) failed')
        return
    end
end

local function get_mcu_dump_len(chip, index, bus_type)
    local cmd = GET_DUMP_LEN_CMD + index
    local channel = CHANNEL_TYPE[bus_type]
    local length = SMC_MCU_DUMP_ADDRESS_LEN
    local ok, rsp = pcall(channel.chip_blkread, chip, cmd, length)
    if not ok or not rsp then
        log:error("SMC get (dump max len) failed, error:%s", rsp)
        return
    end
    local data = bs.new([[<<len:32>>]]):unpack(rsp, true)
    return data.len
end

local function set_dump_start_offset(chip, index, offset, bus_type)
    log:info('set offset is %s', offset)
    local cmd = SET_DUMP_ADDRESS_CMD + index
    local channel = CHANNEL_TYPE[bus_type]
    local ok, rsp = pcall(channel.chip_blkwrite, chip, cmd, string.pack('L', offset))
    if not ok then
        log:debug("SMC set (dump start address) failed, error:%s", rsp)
        return
    end
    return ok
end

local function retry_set_dump_start_offset(chip, index, offset, bus_type)
    local ok
    for _ = 1, 10 do
        ok = set_dump_start_offset(chip, index, offset, bus_type)
        if ok then
            return
        else
            cmn.skynet.sleep(50) --设置失败等待0.5s
        end
    end
    error('set dump start offset failed')
end

local function get_mcu_dump_data(chip, index, bus_type)
    local cmd = GET_DUMP_DATA_CMD + index
    local channel = CHANNEL_TYPE[bus_type]
    local length = bus_type == I3C_BUS_TYPE and I3C_MCU_DUMP_DATA_LEN or SMC_MCU_DUMP_DATA_LEN
    local ok, rsp = pcall(channel.chip_blkread, chip, cmd, length)
    if not ok or not rsp then
        log:debug("SMC get (mcu dump data) failed, error:%s", rsp)
        return
    end
    return response_data_bs:unpack(rsp)
end

function c_npu_board:update_dump_data(print_data, file_path, log_type)
    -- 如果存在日志则需要删除
    local old_log_name
    if  vos.get_file_accessible(file_path) then
        for _, log_name in pairs(utils_core.dir(file_path)) do
            if string.match(log_name, log_type) then
                old_log_name = log_name
                break
            end
        end
    end
    local times_tamp = os.time()  -- 获取当前时间戳
    local date_str = os.date("%Y%m%d%H%M%S", times_tamp)  -- 将时间戳转换为年月日时分秒的格式
    local file_name = string.format('%s%s_%s.bin', file_path, log_type, date_str)
    local file, err = file_sec.open_s(file_name, 'w+')
    if not file then
        log:error('open %s failed, err: %s', log_type, err)
        return
    end
    utils.close(file, pcall(file.write, file, print_data))
    utils_core.chmod_s(file_name, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
    if old_log_name then
        utils.remove_file(file_path .. old_log_name)
    end
end

function c_npu_board:update_dump_info(log_type, file_path, bus_type)
    local chip = self:get_prop('RefSMCChip')
    local index = log_type_lists[bus_type][log_type]
    local len

    for _ = 1, 10 do
        len = get_mcu_dump_len(chip, index, bus_type)
        if len then
            break
        else
            cmn.skynet.sleep(50) --获取失败硬件建议等待0.5s
        end
    end
    if not len or len > log_max_lens[bus_type][log_type] then
        log:error('len is invalid,len: %s,name:%s', len, self.mds_obj.DeviceName)
        return
    end
    log:notice('get len is %s, name:%s', len, self.mds_obj.DeviceName)

    local offset = 0
    local result = {}
    local retries = 30
    local recv_data
    retry_set_dump_start_offset(chip, index, 0, bus_type)
    while offset < len and retries > 0 do
        if self.mds_obj.CurrentUpgradeStatus ~= 0 then
            -- 正在升级MCU时停止收集日志,等待1分钟
            cmn.skynet.sleep(6000)
            log:notice('mcu is upragde, pause collect, Name:%s', self.mds_obj.DeviceName)
            goto continue
        end
        recv_data = get_mcu_dump_data(chip, index, bus_type)
        -- 出现问题需要重新设置起始地址
        if not recv_data or offset ~= recv_data.offset then
            retries = retries - 1
            retry_set_dump_start_offset(chip, index, offset, bus_type)
            cmn.skynet.sleep(10)
        else
            local file_len = bus_type == I3C_BUS_TYPE and I3C_MCU_DUMP_FILE_LEN or SMC_MCU_DUMP_FILE_LEN
            table.insert(result, recv_data.data)
            offset = offset + file_len
            retries = 30
        end
        ::continue::
    end
    if retries <= 0 then
        log:error('[NpuBoard]npu dump is lack, len:%s, expected len is :%s', offset, len)
        return
    end
    local print_data = table.concat(result)
    self:update_dump_data(print_data, file_path, log_type)
end

function c_npu_board:update_dump_log()
    local file_path = string.format('/data/var/log/socboard/%s_%s/',
        self.mds_obj.DeviceName, self.mds_obj.Name)
        utils.mkdir_with_parents(file_path, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
    local ok, err
    local bus_type = self:get_prop('BusType') or 0
    for k, _ in pairs(log_type_lists[bus_type]) do
        ok, err = pcall(function ()
            self:update_dump_info(k, file_path, bus_type)
        end)
        if not ok then
            log:error('get dump log(%s) failed, error :%s', self.mds_obj.DeviceName, err)
        end
    end
    log:notice('get all dump log(%s) end', self.mds_obj.DeviceName)
end

function c_npu_board:on_dump_mcu_log_cb()
    local socboard_path = "/dev/shm/dump_info_tmp/dump_info/LogDump/socboard"
    utils.mkdir_with_parents(socboard_path, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
    local ret = file_sec.check_realpath_before_open_s(socboard_path)
    if ret ~= 0 then
        log:error('check socboard path failed, err:%s', ret)
        return
    end
    local log_path = string.format('/data/var/log/socboard/%s_%s',
        self.mds_obj.DeviceName, self.mds_obj.Name)
    ret = file_sec.check_realpath_before_open_s(log_path)
    if ret ~= 0 then
        log:error('check log path failed, Name: %s, err:%s', self.mds_obj.DeviceName, ret)
        return
    end
    vos.system_s("/bin/cp", "-r", log_path, socboard_path)
end

return c_npu_board