-- 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 singleton = require 'mc.singleton'
local c_drive = require 'drive.drive_object'
local class = require 'mc.class'
local signal = require 'mc.signal'
local log = require 'mc.logging'
local context = require 'mc.context'
local storage_bus = require 'storage_bus'
local nvme_object = require 'nvme.nvme_object'
local common_def = require 'common_def'
local c_drive_collection = class()

function c_drive_collection:ctor()
    self.on_add_drive = c_drive.on_add_object
    self.on_del_drive = c_drive.on_delete_object
    self.on_update_volume_list = signal.new()
    self.on_controller_commu_changed = signal.new()
    self.on_smbios_status_changed = signal.new()
    self.on_add_nvme = nvme_object.on_add_object
    self.on_del_nvme = nvme_object.on_delete_object
    self.nvme_list = {}
end

local function remove_item_from_table(nvme_list, nvme)
    for i, v in ipairs(nvme_list) do
        if v.Slot == nvme.Slot then
            table.remove(nvme_list, i)
            break
        end
    end
end

local function sort_nvme_obj_by_slot(nvme_list)
    table.sort(nvme_list, function (a, b)
        return a.Slot < b.Slot
    end)
end

function c_drive_collection:set_nvme_smart_info_to_defalut()
    for _, nvme_obj in pairs(self.nvme_list) do
        local drive_name = 'Disk' .. nvme_obj.Slot
        local drive = self:get_drive(drive_name)
        if drive then
            drive.AvailableSpare = common_def.INVALID_U8
            drive.UsedPercentage = common_def.INVALID_U8
            drive.CriticalWarning = common_def.INVALID_U8
        end
    end
end

function c_drive_collection:process_smbios_status_changed()
    self.on_smbios_status_changed:on(function (smbios_status)
        if not self.nvme_list or not next(self.nvme_list) then
            log:notice('NVMe list is empty.')
            return
        end
        self:clean_endpoint()
        self:set_nvme_smart_info_to_defalut()
        if smbios_status then
            for _, nvme_obj in pairs(self.nvme_list) do
                nvme_obj.on_smbios_status_changed:emit()
            end
        end
    end)
end

function c_drive_collection:init()
    self.on_controller_commu_changed:on(function(mctp_state, controller_id)
        if mctp_state then
            c_drive.collection:fold(function(acc, obj, key)
                if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
                    obj:start_update_tasks()
                end
            end, {})
        else
            c_drive.collection:fold(function(acc, obj, key)
                if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
                    obj:stop_update_tasks()
                    obj:set_default_values()
                    obj.RefVolumeList = {}
                end
            end, {})
        end
    end)

    self:process_smbios_status_changed()

    self.on_add_drive:on(function(drive)
        drive.on_update_volume_list:on(function(obj)
            self.on_update_volume_list:emit(obj)
        end)
        drive.on_update_diagnose_metric:on(function(obj_name)
            local tmp_bus = storage_bus.get_instance().bus
            local drive_path = '/bmc/kepler/Systems/1/Storage/Drives/' .. obj_name
            tmp_bus:signal(drive_path, 'bmc.kepler.Metric', 'CollectSignal', 'a{ss}ss',
                context.get_context() or {}, 'disk.log', drive_path)
        end)
    end)

    self.on_add_nvme:on(function(nvme)
        log:notice('Nvme %s is inserted', nvme.Slot)
        table.insert(self.nvme_list, nvme)
        sort_nvme_obj_by_slot(self.nvme_list)
        local drive_name = 'Disk' .. nvme.Slot
        local drive = self:get_drive(drive_name)
        if drive then
            drive:nvme_identified(nvme)
        end
    end)

    self.on_del_nvme:on(function(nvme)
        log:notice('Nvme %s is removed', nvme.Slot)
        remove_item_from_table(self.nvme_list, nvme)
        sort_nvme_obj_by_slot(self.nvme_list)
        local drive_name = 'Disk' .. nvme.Slot
        local drive = self:get_drive(drive_name)
        if drive then
            drive:nvme_identified(nil)
        end
    end)
end

function c_drive_collection:clean_endpoint()
    for _, nvme_obj in pairs(self.nvme_list) do
        nvme_obj.nvme_mi_mctp_obj = false
    end
end

function c_drive_collection:get_drive(drive_name)
    return c_drive.collection:find({ Name = drive_name })
end

function c_drive_collection:get_drive_by_physic_id(ref_controller_id, slot_num, enclosure_id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        SlotNumber = slot_num,
        EnclosureId = enclosure_id
    })
end

function c_drive_collection:get_drive_by_id(ref_controller_id, id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        Id = id
    })
end

function c_drive_collection:get_drive_by_device_id(ref_controller_id, device_id)
    return c_drive.collection:find({
        RefControllerId = ref_controller_id,
        device_id = device_id
    })
end

-- 以控制器Id为下标，注意不能多线程访问
local drive_list = {}

function c_drive_collection:get_drives_by_controller_id(controller_id)
    if drive_list[controller_id] == nil then
        drive_list[controller_id] = {}
    else
        for k, _ in pairs(drive_list[controller_id]) do
            drive_list[controller_id][k] = nil
        end
    end
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId == controller_id and obj.identify_pd ~= false and obj.presence ~= 0 then
            table.insert(drive_list[controller_id], obj.Name)
        end
    end, {})
    return drive_list[controller_id]
end

function c_drive_collection:check_and_get_drive(drive_name)
    if not type(drive_name) == "string" then
        log:error("[Storage]drive_name type invalid")
        return
    end
    local drive = self:get_drive(drive_name)

    return drive
end

function c_drive_collection:get_identified_drives()
    local drive_name = {}
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId ~= 0xff and obj.identify_pd ~= false and obj.presence ~= 0 then
            table.insert(drive_name, obj.Name)
        end
    end, {})
    return drive_name
end

-- 查找指定RAID控制器的物理盘信息，并dump物理盘信息到文件中
function c_drive_collection:dump_controller_physical_drive(ctrl_index, fp_w)
    if self:get_count() == 0 then
        fp_w:write('Not Found Physical Drives.\n')
        return
    end

    local count = 0
    c_drive.collection:fold(function(acc, obj, key)
        if obj.Presence ~= 1 then
            goto continue
        end
        if obj.RefControllerId == ctrl_index then
            count = count + 1
            obj:dump_physical_drive(fp_w)
         end

        ::continue::
    end, {})
    if count == 0 then
        fp_w:write('None of physical drives is in position.\n')
    end
end

function c_drive_collection:get_count()
    local count = 0
    c_drive.collection:fold(function(acc, obj, key)
        count = count + 1
    end, {})
    return count
end

function c_drive_collection:get_manufacturer(device_number)
    local drive_obj = c_drive.collection:find({
        Id = device_number - 1
    })

    if not drive_obj then
        log:error("cannot find drive_obj, device_number:%s", device_number)
        return ''
    end

    return drive_obj.Manufacturer
end

function c_drive_collection:update_drive_ref_controller_type_id(ctrl_index, type_id)
    c_drive.collection:fold(function(acc, obj, key)
        if obj.RefControllerId == ctrl_index then
            obj.RefControllerTypeId = type_id
        end
   end, {})
end

function c_drive_collection:get_all_drives()
    local drives = {}
    c_drive.collection:fold(function(acc, obj, key)
        table.insert(drives, obj)
   end, {})
   return drives
end

return singleton(c_drive_collection)
