-- 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.

-- Description: common interfaces for sensor application.
local skynet = require 'skynet'
local log = require 'mc.logging'
local bs = require 'mc.bitstring'
local mc_utils = require 'mc.utils'
local cc = require 'sensor.sensor_const'
local utils = require 'sensor_utils'

local sensor_common = {}
sensor_common.__index = sensor_common

sensor_common.RET_OK = 0
sensor_common.RET_ERROR = -1

function sensor_common.op(caller, fmt, ...)
    log:operation(caller, 'sensor', fmt, ...)
end

local pt = bs.new([[<<
    number,
    all_disabled:1,
    scan_disabled:1,
    change_mode:2,
    reserved:4,
    assert_mask:2/unit:8,
    deassert_mask:2/unit:8
>>]])

function sensor_common.parse_enable_req(data)
    return pt:unpack(data)
end

-- sensor 允许对外修改的有：读值状态，状态，读值，门限，迟滞量
local prop_update_checks = {
    "EntityId",
    "EntityInstance",
    "SensorName",
    "ReadingStatus",
    "Status",
    'Reading',
    'UpperNonrecoverable',
    'UpperCritical',
    'UpperNoncritical',
    'LowerNonrecoverable',
    'LowerNoncritical',
    'LowerCritical',
    'PositiveHysteresis',
    'NegativeHysteresis',
    'OriginalReading'
}
function sensor_common.check_updation(name)
    for i = 1, #prop_update_checks do
        if prop_update_checks[i] == name then
            return true
        end
    end

    return false
end

function sensor_common.compare(unit, a, b)
    if unit & 0xC0 ~= 0 then
        -- 有符号数比较
        a = a < 0 and a or string.unpack('i1', string.pack('I1', a))
        b = b < 0 and b or string.unpack('i1', string.pack('I1', b))
        return (a >= b)
    end
    -- 无符号数比较，要大于等于为true，因此这里要 not
    return not math.ult(a, b)
end

function sensor_common.get_scan_enabled(status, dumped)
    local rs = utils.reading_state:unpack(string.pack('I1', status))
    if not rs then
        log:error('sensor status [0x%02X] is invalid', status)
        return false
    end

    if rs.disable_all == utils.SCAN_DISABLED then
        return false
    end
    if rs.disable_scanning_local == utils.SCAN_DISABLED then
        return false
    end
    if rs.disable_scanning == utils.SCAN_DISABLED then
        return false
    end
    if dumped and rs.disable_access_error == utils.SCAN_DISABLED then
        return false
    end
    if dumped and rs.initial_update_progress == cc.SENSOR_UPDATE_IN_PROGRESS then
        return false
    end
    return true
end

local function convert_by_analog(o, v)
    local unit = sensor_common.unit:unpack(string.pack('I1', o.Unit))
    if not unit then
        log:error('sensor unit 0x%02X is invalid', o.Unit)
        return 0
    end

    local flag = false
    local rexp = utils.tos((o.RBExp >> 4) & 0x0F, 4)
    local bexp = utils.tos(o.RBExp & 0x0F, 4)
    local m = utils.tos(((o.MT & 0xC0) << 2) + o.M, 10)
    local b = utils.tos(((o.BA & 0xC0) << 2) + o.B, 10)
    if unit.analog == 0 then
        return ((m * v) + (b * 10 ^ bexp)) * 10 ^ rexp
    end
    if unit.analog == 1 and v & 0x80 ~= 0 then
        flag = true
        v = v + 1
    end
    if unit.analog == 2 or flag then
        local value = string.unpack('i1', string.pack('I1', v))
        return ((m * value) + (b * 10 ^ bexp)) * 10 ^ rexp
    end

    return 0
end

function sensor_common.convert_raw_to_normal(o, v)
    -- 将门限传感器的原始读值进行转换为可显示值
    local ret = convert_by_analog(o, v)
    local l = o.Linearization & 0x7F
    if l == cc.SENSOR_L_LINEAR then
        return string.format('%.3f', ret)
    elseif l == cc.SENSOR_L_LN then
        return string.format('%.3f', math.log(ret))
    elseif l == cc.SENSOR_L_LOG10 then
        return string.format('%.3f', math.log10(ret))
    elseif l == cc.SENSOR_L_LOG2 then
        return string.format('%.3f', math.log(ret) / math.log(2))
    elseif l == cc.SENSOR_L_E then
        return string.format('%.3f', math.exp(ret))
    elseif l == cc.SENSOR_L_EXP10 then
        return string.format('%.3f', 10 ^ ret)
    elseif l == cc.SENSOR_L_EXP2 then
        return string.format('%.3f', 2 ^ ret)
    elseif l == cc.SENSOR_L_1_X then
        return string.format('%.3f', ret ^ -1)
    elseif l == cc.SENSOR_L_SQR then
        return string.format('%.3f', ret ^ 2)
    elseif l == cc.SENSOR_L_CUBE then
        return string.format('%.3f', ret ^ 3)
    else
        return string.format('%.3f', ret)
    end
end

function sensor_common.convert_normal_to_raw(o, v)
    local unit = sensor_common.unit:unpack(string.pack('I1', o.Unit))
    if not unit then
        log:error('sensor unit 0x%02X is invalid', o.Unit)
        return nil, 'sensor unit is invalid.'
    end

    local value = 0
    local err = nil
    if unit.analog > 2 or o.M == 0 then
        log:info('mock only works for analog sensor and m cannot be equal to 0.')
        return 0
    end
    local m = utils.tos(((o.MT & 0xC0) << 2) + o.M, 10)
    local b = utils.tos(((o.BA & 0xC0) << 2) + o.B, 10)
    local bexp = utils.tos(o.RBExp & 0x0F, 4)
    local rexp = utils.tos((o.RBExp >> 4) & 0x0F, 4)
    value = math.floor(0.5 + (((v / (10 ^ rexp)) - b * (10 ^ bexp)) / m))
    if unit.analog == 0 and value > 255 then
        log:error('mock value [unsigned: %s] is out of range.', value)
        err = string.format('mock value(%s) is out of range, should be (0 ~ 255)', value)
        return nil, err
    end
    if unit.analog ~= 0 and (value > 127 or value < -128) then
        log:error('mock value [signed: %s] is out of range.', value)
        err = string.format('mock value(%s) is out of range, should be (-128 ~ 127)', value)
        return nil, err
    end
    return value
end

function sensor_common.dump_sensors(thresholds, discretes, path)
    local fmt = {
        '0x%-8x', '%-16s', '%-10s', '%-12s', '%-6s', '%-10s', '%-10s',
        '%-10s', '%-8s', '%-10s', '%-10s', '%-6s', '%-6s'
    }
    local dumps = {}
    local item = {}

    -- 格式化标题
    fmt[1] = '%-10s'
    local title = {
        "sensor id", "sensor name", "value", "unit", "status", "lnr", "lc",
        "lnc", "unc", "uc", "unr", "phys", "nhys"
    }
    for j = 1, #title do item[#item+1] = string.format(fmt[j], title[j]) end
    dumps[#dumps+1] = table.concat(item, ' | ') .. '\n'

    -- 格式化对应的采集数据
    fmt[1] = '0x%-8x'
    for i = 1, #thresholds do
        item = {}
        for j = 1, #thresholds[i] do
            item[#item+1] = string.format(fmt[j], thresholds[i][j])
        end
        dumps[#dumps+1] = table.concat(item, ' | ') .. '\n'
        if i % utils.DUMP_SLEEP_PERIOD == 0 then skynet.sleep(utils.DUMP_SLEEP_TIME) end
    end
    for i = 1, #discretes do
        item = {}
        for j = 1, #discretes[i] do
            item[#item+1] = string.format(fmt[j], discretes[i][j])
        end
        dumps[#dumps+1] = table.concat(item, ' | ') .. '\n'
        if i % utils.DUMP_SLEEP_PERIOD == 0 then skynet.sleep(utils.DUMP_SLEEP_TIME) end
    end

    local f, err = require('utils.file').open_s(path, 'w')
    if not f then
        log:error('[dump sensors] open or check file failed, err: %s', err)
        return
    end
    mc_utils.chmod(path, mc_utils.S_IRUSR | mc_utils.S_IWUSR | mc_utils.S_IRGRP)
    for i = 1, #dumps do
        f:write(dumps[i])
        if i % utils.DUMP_SLEEP_PERIOD == 0 then skynet.sleep(utils.DUMP_SLEEP_TIME) end
    end
    f:close()
end

function sensor_common.convert_health(health)
    local map = {
        [utils.HEALTH_NORMAL] = 'OK',
        [utils.HEALTH_MINOR] = 'Minor',
        [utils.HEALTH_MAJOR] = 'Major',
        [utils.HEALTH_CRITICAL] = 'Critical'
    }

    return map[health] or 'OK'
end

function sensor_common.convert_sensor_status(last_status, cur_status)
    -- InTest优先级最高，Starting次之，Enabled/Disabled最低
    if last_status == 'InTest' or cur_status == 'InTest' then
        return 'InTest'
    end
    if last_status == 'Starting' or cur_status == 'Starting' then
        return 'Starting'
    end
    return cur_status
end

function sensor_common.get_sensor_unit_str(unit, base, modifier)
    local us = sensor_common.unit:unpack(string.pack('I1', unit))
    if not us then
        log:error('sensor unit 0x%02X is invalid.', unit)
        return 'unspecified'
    end

    if not sensor_common.unit_desc[base] then
        log:error('sensor base unit 0x%02X is invalid.', base)
        return 'unspecified'
    end

    local sf = string.format
    if us.modifier == 2 then
        return sf('%s*%s', sensor_common.unit_desc[base], sensor_common.unit_desc[modifier])
    elseif us.modifier == 1 then
        return sf('%s/%s', sensor_common.unit_desc[base], sensor_common.unit_desc[modifier])
    else
        return sensor_common.unit_desc[base]
    end
end

sensor_common.assert_mask = bs.new([[<<
    assert_noncritical_lgl:1,
    assert_noncritical_lgh:1,
    assert_critical_lgl:1,
    assert_critical_lgh:1,
    assert_nonrecoverable_lgl:1,
    assert_nonrecoverable_lgh:1,
    assert_noncritical_ugl:1,
    assert_noncritical_ugh:1,
    assert_critical_ugl:1,
    assert_critical_ugh:1,
    assert_nonrecoverable_ugl:1,
    assert_nonrecoverable_ugh:1,
    reading_lower_noncritical:1,
    reading_lower_critical:1,
    reading_lower_nonrecoverable:1,
    _:1
>>]])

sensor_common.deassert_mask = bs.new([[<<
    deassert_noncritical_lgl:1,
    deassert_noncritical_lgh:1,
    deassert_critical_lgl:1,
    deassert_critical_lgh:1,
    deassert_nonrecoverable_lgl:1,
    deassert_nonrecoverable_lgh:1,
    deassert_noncritical_ugl:1,
    deassert_noncritical_ugh:1,
    deassert_critical_ugl:1,
    deassert_critical_ugh:1,
    deassert_nonrecoverable_ugl:1,
    deassert_nonrecoverable_ugh:1,
    reading_upper_noncritical:1,
    reading_upper_critical:1,
    reading_upper_nonrecoverable:1,
    _:1
>>]])

sensor_common.assert_state = bs.new([[<<
    assert_noncritical_lgl:1,
    assert_noncritical_lgh:1,
    assert_critical_lgl:1,
    assert_critical_lgh:1,
    assert_nonrecoverable_lgl:1,
    assert_nonrecoverable_lgh:1,
    assert_noncritical_ugl:1,
    assert_noncritical_ugh:1,
    assert_critical_ugl:1,
    assert_critical_ugh:1,
    assert_nonrecoverable_ugl:1,
    assert_nonrecoverable_ugh:1,
    _:4
>>]])

sensor_common.deassert_state = bs.new([[<<
    deassert_critical_ugl:1,
    deassert_critical_ugh:1,
    deassert_nonrecoverable_ugl:1,
    deassert_nonrecoverable_ugh:1,
    _:4,
    deassert_noncritical_lgl:1,
    deassert_noncritical_lgh:1,
    deassert_critical_lgl:1,
    deassert_critical_lgh:1,
    deassert_nonrecoverable_lgl:1,
    deassert_nonrecoverable_lgh:1,
    deassert_noncritical_ugl:1,
    deassert_noncritical_ugh:1
>>]])

sensor_common.reading_mask = bs.new([[<<
    readable_lower_noncritical:1,
    readable_lower_critical:1,
    readable_lower_nonrecoverable:1,
    readable_upper_noncritical:1,
    readable_upper_critical:1,
    readable_upper_nonrecoverable:1,
    _:2,
    settable_lower_noncritical:1,
    settable_lower_critical:1,
    settable_lower_nonrecoverable:1,
    settable_upper_noncritical:1,
    settable_upper_critical:1,
    settable_upper_nonrecoverable:1,
    _:2
>>]])

sensor_common.unit = bs.new([[<<
    percentage:1,
    modifier:2,
    rate:3,
    analog:2
>>]])

-- IPMI 43.17 标准的传感器单位，每一行8个，从下标0开始
sensor_common.unit_desc = {
    [0] = 'unspecified', 'degrees C', 'degrees F', 'degrees K', 'Volts', 'Amps', 'Watts', 'Joules',
    'Coulombs', 'VA', 'Nits', 'lumen', 'lux', 'Candela', 'kPa', 'PSI',
    'Newton', 'CFM', 'RPM', 'Hz', 'microsecond', 'millisecond', 'second', 'minute',
    'hour', 'day', 'week', 'mil', 'inches', 'feet', 'cu in', 'cu feet',
    'mm', 'cm', 'm', 'cu cm', 'cu m', 'liters', 'fluid ounce', 'radians',
    'steradians', 'revolutions', 'cycles', 'gravities', 'ounce', 'pound', 'ft-lb', 'oz-in',
    'gauss', 'gilberts', 'henry', 'millihenry', 'farad', 'microfarad', 'ohms', 'siemens',
    'mole', 'becquerel', 'PPM', 'reserved', 'Decibels', 'DbA', 'DbC', 'gray',
    'sievert', 'color temp deg K', 'bit', 'kilobit', 'megabit', 'gigabit', 'byte', 'kilobyte',
    'megabyte', 'gigabyte', 'word', 'dword', 'qword', 'line', 'hit', 'miss',
    'retry', 'reset', 'overflow', 'underrun', 'collision', 'packets', 'messages', 'characters',
    'error', 'correctable error', 'uncorrectable error', 'fatal error', 'grams'
}

return sensor_common
