
# CHIPSEC: Platform Security Assessment Framework
# Copyright (c) 2024, Intel Corporation
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; Version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# Contact information:
# chipsec@intel.com
#

# -------------------------------------------------------------------------------
#
# CHIPSEC: Platform Hardware Security Assessment Framework
#
# -------------------------------------------------------------------------------

import os
from enum import Enum
from importlib import import_module
from typing import Dict, Any, List
from chipsec.hal.hal_base import HALBase
from chipsec.library.logger import logger
from chipsec.library.file import get_main_dir
from chipsec.library.strings import make_hex_key_str
from chipsec.library.exceptions import HALNotFoundError, HALInitializationError
if logger().DEBUG:
    import traceback
else:
    traceback = type('traceback', (), {})()  # Create a dummy traceback object if not in debug mode
    traceback.format_exc = lambda: "Traceback not available in non-debug mode"

class Hals:
    """
        HALs manager class
        Search for HAL modules in chipsec/hal/ subdirectories
        Will look for Architecture specific HALs based on mfgid first, then generic HALs in the common directory. If both are found, it will use the Architecture specific HAL.
        For all HAL modules, it will look for a 'haldata' attribute that contains the following information:
            name: dict of HAL class names with keys as the HAL name (e.g., 'memory', 'msr', etc.) and values as the class name (e.g., 'Memory', 'Msr', etc.)
            arch: list of supported architectures (MfgIds) for the HAL module. See MfgIds enum in hal_base.py for available architectures.
    """

    def __init__(self, cs):
        self.cs = cs
        self.hals_import_location = "chipsec.hal."
        self._available_hals = []
        self.update_available_hals() #['mem', 'msr', 'ucode', 'io', 'cpu', 'msgbus', 'mmio', 'iobar', 'igd']

    def available_hals(self) -> list:
        a_hals = []
        for i in self._available_hals:
            a_hals += i['name']
        return a_hals

    def list_loadable_hals(self) -> list:
        loadable_list = []
        if not self._available_hals:
            self.update_available_hals()
        for halobjs in self._available_hals:
            for hal in halobjs['name']:
                try:
                    loadable_list.append(self.find_best_hal_by_name(hal))
                except HALNotFoundError:
                    continue
        return loadable_list

    def __getattr__(self, name):
        best_hal = self.find_best_hal_by_name(name)
        try:
            hal_class = getattr(best_hal['mod'], best_hal['name'][name])
            setattr(self, name, hal_class(self.cs))
        except Exception as err:
            logger().log_debug(traceback.format_exc())
            raise HALInitializationError(f'HAL with name {name} was not able to be initialized: {str(err)}')
        return super(Hals, self).__getattribute__(name)

    def find_best_hal_by_name(self, name:str) -> Any:
        # hal_path = f'{self.hals_import_location}{name}'
        selected_hals = []
        for hal in self._available_hals:
            if name in hal['name'].keys() and self.is_mfgid_in_haldata(hal):
                hal['priority'] = 1
                selected_hals.append(hal)
            elif name in hal['name'].keys() and self.is_any_mfgid_in_haldata(hal):
                hal['priority'] = 2
                selected_hals.append(hal)

        if not selected_hals:
            raise HALNotFoundError(f'HAL with name {name} was not found.')

        return sorted(selected_hals, key=lambda x: x['priority'])[0]

    def is_mfgid_in_haldata(self, haldata: List) -> bool:
        return any(self.cs.Cfg.mfgid in subl.value for subl in haldata['arch'])

    def is_any_mfgid_in_haldata(self, haldata: List) -> bool:
        return HALBase.MfgIds.Any in haldata['arch']

    def update_available_hals(self) -> Dict[str, Any]:
        """Determine available HAL modules"""
        hal_base_dir = os.path.join(get_main_dir(), "chipsec", "hal")
        hal_dirs = [f for f in os.listdir(hal_base_dir) if os.path.isdir(os.path.join(hal_base_dir, f)) and '__' not in f]
        hals = []
        for hal_dir in hal_dirs:
            hals += ([f'{hal_dir}.{i[:-3]}' for i in os.listdir(os.path.join(hal_base_dir, hal_dir)) if i[-3:] == ".py" and not i[:2] == "__"])
        logger().log_debug('[CHIPSEC] Loaded HALs:')
        logger().log_debug('\t' + '\n\t'.join(hals))
        module = None
        halsdata = []
        for hal in hals:
            try:
                hal_path = f'{self.hals_import_location}{hal}'
                module = import_module(hal_path)
                hd = getattr(module, 'haldata')
                hd['mod'] = module
                halsdata.append(hd)
            except ImportError as err:
                # Display the import error and continue to import commands
                logger().log_error(f"Exception occurred during import of {hal}: '{str(err)}'")
                logger().log_debug(traceback.format_exc())
                continue
            except AttributeError as err:
                logger().log_error(f"HAL {hal} has not been updated with 'haldata' attribute: '{str(err)}'")
                continue
        self._available_hals = halsdata




# self.pci = pci.Pci(self)
#         self.mem = physmem.Memory(self)
#         self.msr = msr.Msr(self)
#         self.ucode = ucode.Ucode(self)
#         self.io = io.PortIO(self)
#         self.cpu = cpu.CPU(self)
#         self.msgbus = msgbus.MsgBus(self)
#         self.mmio = mmio.MMIO(self)
#         self.iobar = iobar.IOBAR(self)
#         self.igd = igd.IGD(self)
