-- 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 skynet = require 'skynet'
local class = require 'mc.class'
local mdb = require 'mc.mdb'
local log = require 'mc.logging'
local bs = require "mc.bitstring"
local cmn = require 'common'
local ctx = require 'mc.context'
local utils = require 'mc.utils'
local client = cmn.client
local file_sec = require 'utils.file'
local fw_manager = require 'fw_manager'
local chip_lock_singleton = require 'chip_lock'

---@class SRUpgrade @自描述文件SR升级
local sr_upgrade = class()

local FWINVENTORY_PATH <const> = '/bmc/kepler/UpdateService/FirmwareInventory/'
local FIRMWARE_INFO_INTERFACE <const> = 'bmc.kepler.UpdateService.FirmwareInfo'

local EEP_HEADER_LEN<const> = 128

local bs_header = bs.new([[<<
    standard_code:12/string,
    spec_version:8,
    elabel_offset:16,
    sys_desc_offset:16,
    inner_area_offset:16,
    psr_offset:16,
    csr_offset:16,
    digital_sign_offset:16,
    _:73/string,
    sr_version:2/string,
    component_uid:24/string,
    verification_code:4/string
>>]])

-- 向FirmwareInventory注册SR版本信息
function sr_upgrade:register_firmware_info()
    cmn.skynet.fork(function ()
        local param = {
            Id = string.format('SR_%s_%s', self.mds_obj.Type, self.position),
            Name = self.name,
            Version = self:get_version(),
            BuildNum = '',
            ReleaseDate = '',
            LowestSupportedVersion = '',
            SoftwareId = self.mds_obj.SoftwareId and self.mds_obj.SoftwareId or '',
            Manufacturer = 'Huawei',
            Location = '',
            State = 'Enabled',
            Severity = 'Informational'
        }
        local retries = 0
        local ok
        repeat
            ok, _ = client:PFirmwareInventoryFirmwareInventoryAdd(ctx.new(),
                param, true, 1, 10) -- SR固件包最大1MB左右，预留10MB
            if not ok then
                retries = retries + 1
                cmn.skynet.sleep(100)
            end
        until ok or retries > 120  -- 最多重试2分钟
        if retries > 120 then
            log:error('[SRUpgrade] register SR(%s:%s) to firmware inventory fail', param.Id, param.Name)
            return
        end
        log:notice('[SRUpgrade] register SR(%s:%s) to firmware inventory success', param.Id, param.Name)

        self.fw_obj = mdb.get_object(self.bus, FWINVENTORY_PATH .. param.Id, FIRMWARE_INFO_INTERFACE)
        self.id = param.Id -- 注册成功后才保存id
    end)
end

-- 向FirmwareInventory注销SR版本信息
function sr_upgrade:unregister_firmware_info()
    if self.id then
        local ok, _ = client:PFirmwareInventoryFirmwareInventoryDelete(ctx.new(), self.id)
        if ok then
            log:notice('[SRUpgrade] unregister SR(%s) to firmware inventory success', self.id)
            self.id = nil
        else
            log:error('[SRUpgrade] unregister SR(%s) to firmware inventory fail', self.id)
        end
    end
end

function sr_upgrade:get_version()
    if self.mds_obj.Version then
        return self.mds_obj.Version
    end
    return 'NA'
end

local function check_header(header)
    local OFFSET_MAX<const> = 25000
    if header.sys_desc_offset >= OFFSET_MAX or header.elabel_offset >=
        OFFSET_MAX or header.psr_offset >= OFFSET_MAX or header.csr_offset >=
        OFFSET_MAX or header.inner_area_offset >= OFFSET_MAX then
        return false
    end
    return true
end

function sr_upgrade:write_protect_proc(protect_value)
    local ok, rsp
    -- 因为MCU复位需要约8s，所以重试3次，每次延时5s
    for _ = 1, 3 do
        ok, rsp = pcall(function ()
            self.mds_obj.WriteProtect = protect_value
        end)
        if not ok then
            log:error('[write_protect_proc] set write protect is [%s] failed, error: %s', protect_value, rsp)
            goto continue
        end
        cmn.skynet.sleep(10)
        ok, rsp = pcall(function ()
            return self.mds_obj.WriteProtect == protect_value
        end)
        if ok and rsp then
            log:notice('[write_protect_proc] get write protect == %s', protect_value)
            return true
        end
        log:notice('[write_protect_proc] get write protect ~= %s, rsp = %s', protect_value, rsp)
        ::continue::
        cmn.skynet.sleep(500)
    end
    return false
end

-- EEPROM写保护，打开
function sr_upgrade:write_protect_open()
    return self:write_protect_proc(1)
end

-- EEPROM写保护，关闭
function sr_upgrade:write_protect_close()
    return self:write_protect_proc(0)
end

function sr_upgrade:chip_read(offset, length)
    -- 存储SR文件的器件
    local chip = self.mds_obj.StorageChip
    if not chip then
        log:error('[SRUpgrade] chip is nil.')
        return false
    end

    local ok, ret = pcall(function ()
        return chip:Read(ctx.get_context_or_default(), offset, length)
    end)
    if not ok then
        log:error('[SRUpgrade] chip read error: %s', ret)
        return false
    end
    return true, ret
end

function sr_upgrade:chip_write(offset, data)
    -- 存储SR文件的器件
    local chip = self.mds_obj.StorageChip
    if not chip then
        log:error('[SRUpgrade] chip is nil.')
        return false
    end

    local ok, err = pcall(function ()
        return chip:Write(ctx.new(), offset, data)
    end)
    if not ok then
        log:error('[SRUpgrade] chip write error: %s,write protect: %s', err, self.mds_obj.WriteProtect)
        return false
    end
    return true
end

function sr_upgrade:write_data(offset, data_bin)
    local retries = 0
    local ok
    repeat
        cmn.skynet.sleep(100)
        -- 确认写保护状态，未关闭则重新发送命令
        if self.mds_obj.WriteProtect == 0 then
            --写入数据
            ok = self:chip_write(offset, data_bin)
        else
            -- 等待1s防止mcu响应不过来
            cmn.skynet.sleep(100)
            self:write_protect_close()
        end
        retries = retries + 1
    until ok or retries > 5
    if not ok then
        return false
    end
    return true
end

function sr_upgrade:chip_lock(ctx, lock_time)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():lock(self.mds_obj.StorageLockChip, ctx, lock_time)
    end)
end

function sr_upgrade:chip_unlock(ctx)
    return pcall(function (...)
        return chip_lock_singleton.get_instance():unlock(self.mds_obj.StorageLockChip, ctx)
    end)
end

function sr_upgrade:check_eeprom_data(offset, data_writed)
    local read_ret, read_data = self:chip_read(offset, #tostring(data_writed))
    if not read_ret then
        log:error('[SRUpgrade] read back eeprom date failed')
        return false
    end

    return read_data == data_writed
end

local function write_to_eeprom(self, header_bin, header, file)
    --写入header 
    if not self:write_data(0, header_bin) then
        log:error('[SRUpgrade] :write_header failed')
        return false
    end
    log:notice('[SRUpgrade] write header completed, offset: 0, length: %s', #header_bin)

    if not self:check_eeprom_data(0, header_bin) then
        log:error('[SRUpgrade] check header failed')
        return false
    end
    log:notice('[SRUpgrade] read back header and check it successfully')

    -- Data
    -- Header中的 inner_area_offset对应OEM定制区的偏移
    --            csr_offset字段保存了CSR数据在二进制文件中的偏移
    --            psr_offset字段保存了PSR数据在二进制文件中的偏移
    local offset = header.csr_offset
    if header.csr_offset == 0 then
        offset = header.psr_offset
    end
    -- inner_area_offset非0时表示固件包含OEM区域，则从OEM区域开始读写
    if header.inner_area_offset ~= 0 then
        offset = header.inner_area_offset
    end
    offset = offset * 8 -- 单位：8Bytes
    file:seek('set', offset)
    -- 从偏移开始读取剩余所有数据
    local data_bin = file:read('a')
    local ok = self:write_data(offset, data_bin)
    if not ok then
        log:error('[SRUpgrade]write data failed, offset: %s', offset)
        return false
    end
    log:notice('[SRUpgrade] write data completed, offset: %s, length: %s', offset, #data_bin)

    if not self:check_eeprom_data(offset, data_bin) then
        log:error('[SRUpgrade] check data failed')
        return false
    end
    log:notice('[SRUpgrade] read back data and check it successfully')
    return true
end

local function upgrade_sr(self, path)
    local old_version = self.fw_obj and self.fw_obj.Version or ''
    log:notice('[SRUpgrade] upgrade start with version %s, uid: %s', old_version, self.mds_obj.UID)
    local file = file_sec.open_s(path, 'r')
    if not file then return false end

    return utils.safe_close_file(file, function()
        -- Header
        local header_bin = file:read(EEP_HEADER_LEN)
        local header = bs_header:unpack(header_bin)
        if not check_header(header) then
            log:error('[SRUpgrade]header check falied')
            return false
        end

        -- 关闭写保护
        if not self:write_protect_close() then
            log:error('[SRUpgrade] close write-protection failed')
            return false
        end
        log:notice('[SRUpgrade]EEPROM write protection turn off')

        local is_write_succ = false
        for i = 1, 3 do
            if write_to_eeprom(self, header_bin, header, file) then
                log:notice('[SRUpgrade] write file to eeprom successfully, totally retry %s times', i)
                is_write_succ = true
                break
            end
        end

        self:write_protect_open()
        log:notice('[SRUpgrade]EEPROM write protection turn on')

        if not is_write_succ then
            log:error('[SRUpgrade] upgrade failed, uid: %s', self.mds_obj.UID)
            return false
        end
        log:notice('[SRUpgrade] upgrade completed, uid: %s', self.mds_obj.UID)
        return true
    end)
end

function sr_upgrade:upgrade(path)
    local lock_time_out = 600
    local ok, ret_code
    while lock_time_out > 0 do
        ok, ret_code = self:chip_lock(require 'mc.context'.new(), 600)
        if ok and ret_code == 0 then
            local upgrade_ret = upgrade_sr(self, path)
            -- 硬件总线解锁
            ok, ret_code = self:chip_unlock(require 'mc.context'.new())
            if not ok or ret_code ~= 0 then
                log:error("[sr upgrade] set lock status to 0 failed, ret code:%s", ret_code)
            end
            return upgrade_ret
        end
        log:debug("[sr upgrade] get chip lock failed")
        lock_time_out = lock_time_out - 1
        skynet.sleep(100)
    end

    log:error("[sr upgrade] upgrade failed because can not get chip lock")
    return false
end

function sr_upgrade:ctor(mds_obj, bus, position)
    self.bus = bus
    self.mds_obj = mds_obj
    self.position = position
    local device_name = fw_manager:get_device_name_by_position(mds_obj.Type, position)
    self.name = device_name ~= 'NA' and device_name .. ' CSR' or nil
end

function sr_upgrade:init()
    if self.name then
        -- SR向firmware_mgmt注册优化：
        -- 1. 如果Board对象先于SRUpgrade对象分发，self.name不为nil，则直接向FirmwareInventory注册
        -- 2. 如果Board对象晚于SRUpgrade对象分发，self.name为nil，则延时到SR所有对象分发完之后再补注册
        self:register_firmware_info()
    end
end

return sr_upgrade
