-- 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 object_path = require 'dbus.object_path'
local dbus_interface = require 'dbus.interface'
local log = require 'mc.logging'
local class = require 'mc.class'
local json = require 'cjson'
local GVariant = require 'mc.gvariant'
local new_string = GVariant.new_string
local new_int64 = GVariant.new_int64
local new_double = GVariant.new_double
local new_bool = GVariant.new_bool
local new_gvariant = GVariant.new
local json_null = json.null
local escape = object_path.escape
local unescape = object_path.unescape
local is_variant = GVariant.is_variant
local variant_type = GVariant.type
local json_decode = json.decode
local json_encode = json.encode

local ODATA_ID<const> = '@odata.id'
local ODATA_TYPE<const> = '@odata.type'
local ODATA_CONTEXT<const> = '@odata.context'
local SMS_INTERFACE<const> = 'bmc.kepler.sms'
local SMS_REDFISH_INTERFACE<const> = 'bmc.kepler.sms.redfish'
local BMC_PATH<const> = '/bmc/kepler/Systems/1/Sms/1/ComputerSystem'

local function to_bmc_url(odata_id, skip_unknown_resource)
    if not odata_id then
        return nil
    end

    local url_tail = odata_id:match('^/redfish/v1/Sms/1(/.+)$')
    if url_tail then
        return BMC_PATH .. escape(url_tail)
    end

    if skip_unknown_resource then
        return nil
    end

    return escape(odata_id)
end

local function for_each(props, cb)
    for name, value in pairs(props) do
        if name == ODATA_ID then
            value = to_bmc_url(value)
        end
        cb(name, value)
    end
end

local update_info = {}
update_info.__index = update_info

function update_info.new()
    return setmetatable({add = {}, update = {}, delete = {}, is_empty = true}, update_info)
end

function update_info:clean()
    if self.is_empty then
        return
    end

    self.add = {}
    self.update = {}
    self.delete = {}
    self.is_empty = false
end

function update_info:add_interface(iface)
    self.add[iface] = true
    self.delete[iface] = nil
    self.update[iface] = nil
    self.is_empty = false
end

function update_info:del_interface(iface)
    self.add[iface] = nil
    self.update[iface] = nil
    self.delete[iface] = true
    self.is_empty = false
end

function update_info:update_interface(iface, prop)
    if self.add[iface] or self.delete[iface] then
        return
    end

    local props = self.update[iface]
    if not props then
        props = {}
        self.update[iface] = props
    end
    props[prop] = true
    self.is_empty = false
end

local function dict_to_array(datas)
    local iface = {}
    for i in pairs(datas) do
        iface[#iface + 1] = i
    end
    return iface
end

function update_info:emit(bus, path)
    if self.is_empty then
        return
    end

    if next(self.add) then
        bus:emit_interfaces(true, path, dict_to_array(self.add))
        self.add = {}
    end

    if next(self.delete) then
        bus:emit_interfaces(false, path, dict_to_array(self.delete))
        self.delete = {}
    end

    for iface, props in pairs(self.update) do
        bus:emit_properties_changed(path, iface, dict_to_array(props))
    end

    self.update = {}
    self.is_empty = true
end

local sms_objects = class()

function sms_objects:ctor(bus)
    self.bus = bus
    self.datas = {}
    self.info = update_info.new()
end

function sms_objects:init()
end

local function json_type_to_gvariant_type(val)
    local t = type(val)
    if t == 'string' then
        return 's'
    elseif t == 'number' then
        return math.floor(val) == val and 't' or 'd'
    elseif t == 'boolean' then
        return 'b'
    elseif t == 'userdata' and val == json_null then
        return 'null'
    end
    return nil
end

local function json_array_to_gvariant(val)
    local sub_type = json_type_to_gvariant_type(val[1])
    if not sub_type then
        local sub_vals = {}
        for i, v in ipairs(val) do
            sub_vals[i] = new_gvariant('s', json_encode(v))
        end
        return new_gvariant('av', sub_vals)
    end
    return new_gvariant('a' .. sub_type, val)
end

local function json_to_gvariant(val)
    if val == json_null then
        return new_string('null')
    end

    local t = type(val)
    if t == 'string' then
        return new_string(val)
    elseif t == 'number' then
        return math.floor(val) == val and new_int64(val) or new_double(val)
    elseif t == 'boolean' then
        return new_bool(val)
    elseif t == 'table' then
        if #val > 0 then -- 数组
            return json_array_to_gvariant(val)
        elseif next(val) == nil then -- 空表
            return new_gvariant('ai', {})
        end
    end
end

local function is_sub_object(value)
    return type(value) == 'table' and #value == 0 and next(value) ~= nil
end

local function new_dbus_value(name, value)
    local v = json_to_gvariant(value)
    if not v then
        error(string.format('json value to gvariant failed: prop=%s, type=%s, value=%s', name,
            type(value), value))
    end
    return v
end

local function get_value(val)
    if type(val) == 'userdata' and is_variant(val) then
        return get_value(val:value())
    elseif val == 'null' then
        return json_null
    else
        return val
    end
end

local function equal(a, b)
    if type(a) ~= 'table' or type(b) ~= 'table' then
        return a == b
    end

    if #a ~= #b then
        return false
    end

    for k, v in pairs(a) do
        if not equal(v, b[k]) then
            return false
        end
    end

    return true
end

local function is_json_array(val)
    local v_type = variant_type(val)
    return #v_type > 1 and v_type == 'av'
end

local function to_json_value(val)
    if type(val) == 'userdata' and is_variant(val) then
        if not is_json_array(val) then
            return to_json_value(val:value())
        end

        local res = to_json_value(val:value())
        for k, v in ipairs(res) do
            res[k] = json_decode(v:value())
        end
        return res
    elseif val == 'null' then
        return json_null
    else
        return val
    end
end

local function iface_to_json(result, iface, i)
    local data = result
    if iface ~= SMS_INTERFACE and iface ~= SMS_REDFISH_INTERFACE then
        local sub = iface:sub(#SMS_REDFISH_INTERFACE + 2)
        if not sub then
            return
        end

        for name in sub:gmatch('([^.]+)') do
            if data[name] == nil then
                data[name] = {}
            end
            data = data[name]
        end
    end

    for name, prop in pairs(i.properties) do
        local un_es_name = unescape(name)
        local value = to_json_value(prop:get())
        if un_es_name == ODATA_ID then
            data[un_es_name] = unescape(value)
        else
            data[un_es_name] = value
        end
    end
end

local function object_to_json(object, ifaces)
    local json_data = {}
    local iface_count = 0
    if ifaces and #ifaces > 0 then
        for _, iface in ipairs(ifaces) do
            if object.ifaces[iface] then
                iface_to_json(json_data, iface, object.ifaces[iface])
                iface_count = iface_count + 1
            end
        end
    else
        for iface, props in pairs(object.ifaces) do
            iface_to_json(json_data, iface, props)
            iface_count = iface_count + 1
        end
    end

    if iface_count > 0 then
        return json_data
    end
end

function sms_objects:update(redfish_data)
    local path = to_bmc_url(redfish_data[ODATA_ID], true)
    if not path then
        return
    end

    self.info:clean()
    local bus = self.bus.bus
    local ok, err = pcall(function()
        local object = bus.tree:find_object(path)
        if not object then
            bus:insert(path, function(obj)
                self:create_data(obj, redfish_data)
            end):object_manager()
        else
            self:update_data(object, self.datas[path], redfish_data)
        end
    end)
    if not ok then
        log:error('sms_objects: update redfish data failed, %s', err)
    end
    self.info:emit(bus, path)
end

local skip_odatas = {}
skip_odatas.__index = skip_odatas

function skip_odatas.new(data)
    local value = {
        odata_id = data[ODATA_ID],
        odata_type = data[ODATA_TYPE],
        odata_context = data[ODATA_CONTEXT],
        data = data
    }
    data[ODATA_ID] = nil
    data[ODATA_TYPE] = nil
    data[ODATA_CONTEXT] = nil
    return setmetatable(value, skip_odatas)
end

function skip_odatas:__close()
    self.data[ODATA_ID] = self.odata_id
    self.data[ODATA_TYPE] = self.odata_type
    self.data[ODATA_CONTEXT] = self.odata_context
end

function sms_objects:create_data(object, data)
    local s = skip_odatas.new(data)
    pcall(function()
        self:create_interface(object, SMS_INTERFACE, {
            [ODATA_ID] = s.odata_id,
            [ODATA_TYPE] = s.odata_type,
            [ODATA_CONTEXT] = s.odata_context
        }, function(i)
            i:add_m('to_json', 'as', 's', function(ctx)
                local ifaces = ctx.req:read()
                return ctx:reply(json_encode(object_to_json(object, ifaces)))
            end)
        end)
        self.datas[object.path] = self:create_interface(object, SMS_REDFISH_INTERFACE, data)
    end)
    if s then 
        s:__close()
    end
end

function sms_objects:update_data(object, data, new_data)
    -- 这些不允许更新
    local s = skip_odatas.new(new_data)
    pcall(function()
        self:update_interface(object, SMS_REDFISH_INTERFACE, data, new_data)
    end)
    if s then 
        s:__close()
    end
end

function sms_objects:update_interface(object, iface, data, new_data)
    for_each(new_data, function(name, new_value)
        local old_value = data[name]
        local es_name = escape(name)
        if old_value == nil then
            if is_sub_object(new_value) then
                local sub_iface = string.format('%s.%s', iface, es_name)
                data[name] = self:create_interface(object, sub_iface, new_data)
                return
            end

            data[name] = new_dbus_value(name, new_value)
            object.ifaces[iface]:add_p(es_name, 'v', function()
                return data[name]
            end)
            self.info:update_interface(iface, es_name)
        elseif is_sub_object(old_value) then
            local sub_iface = string.format('%s.%s', iface, es_name)
            if is_sub_object(new_value) then
                self:update_interface(object, sub_iface, old_value, new_value)
                return
            end
            object:unregister(sub_iface)
            self.info:del_interface(sub_iface)
        elseif equal(get_value(old_value), new_value) then
            return
        end

        data[name] = new_dbus_value(name, new_value)
        self.info:update_interface(iface, es_name)
    end)
end

function sms_objects:create_interface(object, iface, values, cb)
    local i = dbus_interface.new(iface, true)

    local data = {}
    for_each(values, function(name, value)
        local es_name = escape(name)
        if is_sub_object(value) then
            local sub_iface = string.format('%s.%s', iface, es_name)
            data[name] = self:create_interface(object, sub_iface, value)
        else
            data[name] = new_dbus_value(name, value)
            i:add_p(es_name, 'v', function()
                return data[name]
            end)
        end
    end)

    if next(i.properties) then
        if cb then
            cb(i)
        end
        object:add_interface(i)
        self.info:add_interface(iface)
    end

    return data
end

sms_objects.to_bmc_url = to_bmc_url
sms_objects.object_to_json = object_to_json

function sms_objects.to_bmc_interface(props)
    if not props or #props == 0 then
        return SMS_REDFISH_INTERFACE
    end

    return string.format('%s.%s', SMS_REDFISH_INTERFACE, props)
end

return sms_objects
