-- 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 bs = require 'mc.bitstring'
local log = require 'mc.logging'
local utils = require 'mc.utils'
local skynet = require 'skynet'
local crc8 = require 'mc.crc8'
local context = require 'mc.context'

local STD_SMBUS_REQ_COMMAND_CODE <const> = 0x20 -- smbus读/写请求命令码
local STD_SMBUS_RSP_COMMAND_CODE <const> = 0x21 -- smbus读/写响应命令码
local CHIP_BUFFER_LEN <const> = 64
local HEADER_LEN = 12

local request_header_bs <const> = bs.new([[<<
    lun,
    arg,
    opcode:16,
    offset:32,
    length:32,
    data/string
>>]])

local response_data_bs <const> = bs.new([[<<
    error_code:16,
    opcode:16,
    total_length:32,
    length:32,
    data/string
>>]])

local success_code <const> = 0
local error_code <const> = {
    [0x1] = 'opcode not support',
    [0x2] = 'parameter error',
    [0x3] = 'internal error',
    [0xF] = 'bus busy'
}

local elabel_args <const> = {
    ChassisType = 0x10,
    ChassisPartNumber = 0x11,
    ChassisSerialNumber = 0x12,
    MfgDate = 0x20,
    BoardManufacturer = 0x21,
    BoardProductName = 0x22,
    BoardSerialNumber = 0x23,
    BoardPartNumber = 0x24,
    BoardFRUFileID = 0x25,
    BoardCustomInfo = 0x26,
    Board_IssueNumber = 0x27,
    Board_CLEICode = 0x28,
    Board_BOM = 0x29,
    Board_Model = 0x2A,
    ManufacturerName = 0x30,
    ProductName = 0x31,
    ProductPartNumber = 0x32,
    ProductVersion = 0x33,
    ProductSerialNumber = 0x34,
    AssetTag = 0x35,
    ProductFRUFileID = 0x36,
    SystemManufacturerName = 0x60,
    SystemProductName = 0x61,
    SystemVersion = 0x62,
    SystemSerialNumber = 0x63
}
elabel_args.__index = elabel_args

local std_smbus = { slave_address = 0xD4 }

std_smbus.__index = std_smbus

function std_smbus.new(chip)
    return setmetatable({ chip = chip, buffer_len = CHIP_BUFFER_LEN, capability = { [0x00] = 1 }, process_queue = {} },
        std_smbus)
end

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

-- chip_write_read 写入指定长的数据
local function chip_write_read(chip, addr, write_cmd, data, read_cmd, read_len)
    -- write data
    local check_buf = table.concat({ string.char(addr), string.char(write_cmd), data })
    local crc = crc8(check_buf)
    local value = chip:ComboWriteRead(context.new(), write_cmd, data .. string.pack('B', crc), read_cmd, read_len)
    -- read_data
    check_buf = table.concat({ string.char(addr), string.char(read_cmd), string.char(addr | 0x01),
        value:sub(1, #value - 1) })
    if not value or not check_data(value:sub(#value, #value):byte(), check_buf) then
        error(table.concat({ '[smbus]read commad(0x', string.format('%02x', read_cmd), ') failed' }))
    end
    return value
end

-- chip_blkwrite 从Chip对象中写一个块的信息，第一位为数据长度
local function chip_blkwrite_read(chip, addr, write_cmd, data, read_cmd, read_len)
    local value = chip_write_read(chip, addr, write_cmd, string.pack('B', #data) .. data, read_cmd, read_len + 2)
    return value:sub(2, #value - 1)
end

function std_smbus:send_and_receive(head, read_len)
    local retry_count = 3
    for _ = 1, retry_count do
        local ok, recv_data = pcall(function()
            local recv_data = response_data_bs:unpack(chip_blkwrite_read(self.chip, self.slave_address,
                STD_SMBUS_REQ_COMMAND_CODE, request_header_bs:pack(head), STD_SMBUS_RSP_COMMAND_CODE,
                read_len + HEADER_LEN))
            if recv_data.error_code ~= success_code then
                error(error_code[recv_data.error_code])
            end
            return recv_data
        end)
        if ok then
            return recv_data
        end
    end
    return false
end

function std_smbus:_send_and_receive_request_in_frames(head, read_len)
    local data = ''
    local recv_data
    if not self.capability[head.opcode] then
        log:debug('not support opcode = 0x%x', head.opcode)
        return data
    end
    repeat
        recv_data = self:send_and_receive(head, read_len)
        if not recv_data then
            error('send and receive failed')
        end
        head.offset = head.offset + read_len
        data = data .. recv_data.data:sub(1, recv_data.length)
        -- 获取剩余长度
        read_len = math.min(recv_data.total_length - read_len, self.buffer_len)
        head.length = read_len
    until read_len <= 0
    return data
end

function std_smbus:_send_data(...)
    local co = coroutine.running()
    self.process_queue[#self.process_queue + 1] = co
    if self.process_queue[1] ~= co then
        skynet.wait(co)
    end
    local ok, result = pcall(self._send_and_receive_request_in_frames, self, ...)
    table.remove(self.process_queue, 1)
    if self.process_queue[1] ~= nil then
        skynet.wakeup(self.process_queue[1])
    end
    if not ok then
        error(result)
    end
    return result
end

function std_smbus:GetCapability()
    local recv_data = self:_send_data({
        lun = 0x80,
        arg = 0x00,
        opcode = 0x0000,
        offset = 0x00,
        length = self.buffer_len - HEADER_LEN,
        data = ''
    }, self.buffer_len - HEADER_LEN)

    local resp = bs.new([[<<identify:12,version:4,type,number:16,opcodes/string>>]]):unpack(recv_data, true)
    local tmp = { string.unpack(string.rep('I2', #resp.opcodes // 2), resp.opcodes) }
    for key, value in ipairs(tmp) do
        self.capability[value] = 1
    end
end

function std_smbus:GetMacAddress()
    local recv_data = self:_send_data({
        lun = 0x80,
        arg = 0x00,
        opcode = 0x101B,
        offset = 0x00,
        length = self.buffer_len - HEADER_LEN,
        data = ''
    }, self.buffer_len - HEADER_LEN)
    if not recv_data then
        return {}
    end
    local data
    local mac_info = {}
    for i = 1, #recv_data, 9 do
        data = bs.new([[<<port_id,pf_id:16/big,mac_addr:6/string>>]]):unpack(recv_data:sub(i, i + 8), true)
        table.insert(mac_info,
            { data.port_id, data.pf_id,
                string.format('%X:%X:%X:%X:%X:%X', string.unpack(string.rep('B', 6), data.mac_addr)) })
    end
    return mac_info
end

-- 获取电子标签
function std_smbus:GetNetElabel(callback)
    local elabels = {}
    local elabel_args_cp = setmetatable({}, elabel_args)
    local ok, data
    while true do
        for elabel_prop, arg in pairs(elabel_args) do
            ok, data = pcall(function()
                return self:_send_data({
                    lun = 0x80,
                    arg = arg,
                    opcode = 0x1035,
                    offset = 0x00,
                    length = self.buffer_len - HEADER_LEN,
                    data = ''
                }, self.buffer_len - HEADER_LEN)
            end)
            if ok and data then
                elabels[elabel_prop] = data
                elabel_args_cp[elabel_prop] = nil
            end
        end

        callback(elabels)
        if #elabel_args_cp == 0 then
            return elabels
        end
        elabels = {}
    end
end

local MCU = { std_smbus = std_smbus }

return MCU
