#!/usr/bin/env python3
__package__ = 'configurator.pyodide'

import json
import logging
import re
import os
from copy import deepcopy

import elementpath
from defusedxml.lxml import fromstring
from bs4 import BeautifulSoup

from . import convert_result, nuc11_board, scenario_json_schema, nuc11_board_path


def get_dynamic_scenario(board):
    """

    :type board: str
    :param board: board xml text
    """
    board_xml = fromstring(board)

    def get_enum(source, options, option_names, obj_type):
        elements = [str(x) for x in elementpath.select(source, options) if x]
        element_names = [str(x) for x in elementpath.select(source, option_names) if x]
        elements = list(set(zip(elements, element_names)))
        if not elements:
            elements = [('', '')]
        # TODO: Add more converters if needed
        enum_type_convert = {'integer': lambda x: int(x) if x else 0}
        if obj_type in enum_type_convert.keys():
            elements = [(enum_type_convert[obj_type](x[0]), x[1]) for x in elements]
        return elements

    def dynamic_enum(**enum_setting):
        # value from env
        function, source = [
            {"get_enum": get_enum, "board_xml": board_xml}[enum_setting[key]]
            for key in ['function', 'source']
        ]
        # value from given
        selector, name_selector, sorted_func, obj_type = [enum_setting[key] for key in ['selector', 'name-selector', 'sorted', 'type']]

        # get enum data
        enum = function(source, selector, name_selector, obj_type)
        if sorted_func:
            fn = eval(sorted_func)
            enum = sorted(enum, key=lambda x: fn(x[0]))
        return zip(*enum)

    def dynamic_enum_apply(obj):
        # get json schema enum obj
        if 'enum' in obj and isinstance(obj['enum'], dict):
            enum_setting = obj['enum']
            # check enum obj type
            if enum_setting['type'] == 'dynamicEnum':
                enum_setting['type'] = obj.get('type', '')
                # replace json schema obj enum field data
                enum, enum_names = dynamic_enum(**enum_setting)
                obj['enum'] = enum
                obj['enumNames'] = enum_names
        return obj

    data = json.loads(scenario_json_schema, object_hook=dynamic_enum_apply)

    form_schemas = {}
    tab_types = ['HV', 'PreLaunchedVM', 'ServiceVM', 'PostLaunchedVM']
    form_types = ['BasicConfigType', 'AdvancedConfigType']
    for tab_type in tab_types:
        form_schemas[tab_type] = {}
        for form_type in form_types:
            form_schema = deepcopy(data)
            current_form_type_schema_obj = form_schema['definitions'][f'{tab_type}{form_type}']
            for key in ['type', 'required', 'properties']:
                if key == 'required' and key not in current_form_type_schema_obj:
                    form_schema[key] = []
                    continue
                form_schema[key] = current_form_type_schema_obj[key]
            form_schemas[tab_type][form_type] = form_schema

    return form_schemas


def get_cat_info(soup):
    threads = soup.select('core thread')
    threads = {thread.attrs['id']: thread.select_one('cpu_id').text for thread in threads}
    caches = soup.select('caches cache')
    cat_info = []
    for cache in caches:
        cache_level = int(cache.attrs['level'])

        # ignore cache_level 1 and single core cache region
        if cache_level == 1 or len(processors := cache.select('processors processor')) <= 1:
            continue
        # ignore no CAT capability cache region
        if cache.select_one('#CAT') is None:
            continue

        capacity_mask_length = cache.select_one('capability capacity_mask_length')
        capacity_mask_length = int(capacity_mask_length.text)

        processors = [int(threads[processor.text]) for processor in processors]
        processors.sort()
        cache_info = {
            'id': cache.attrs['id'],
            'level': cache_level,
            'type': cache.attrs['type'],
            'cache_size': int(cache.select_one('cache_size').text),
            'capacity_mask_length': capacity_mask_length,
            'processors': processors,
        }
        cat_info.append(cache_info)
    cat_info.sort(key=lambda x: int(x['id'], 16))
    cat_info.sort(key=lambda x: x['level'], reverse=True)
    return cat_info


def get_board_info(board, path):
    soup = BeautifulSoup(board, 'xml')
    # Workaround: The pyodide thinks it runs under os.name == posix.
    # So os.path.basename will not work on windows.
    # Here we replace all '\' with '/' to make it work (most of the time).
    try:
        path = path.replace('\\', '/')
        fname = os.path.basename(path)
        basename, _ = os.path.splitext(fname)
        board_name = basename + '.xml' if basename.endswith('.board') \
            else basename + '.board.xml'
    except Exception as e:
        logging.warning(e)
        board_name = 'default'
    result = {
        'name': board_name,
        'content': board,
        'CAT_INFO': get_cat_info(soup),
        'BIOS_INFO': soup.select_one('BIOS_INFO').text,
        'BASE_BOARD_INFO': soup.select_one('BASE_BOARD_INFO').text
    }
    return result


def load_board(board, path):
    result = {
        'scenarioJSONSchema': get_dynamic_scenario(board),
        'boardInfo': get_board_info(board, path)
    }
    return convert_result(result)


def test():
    load_board(nuc11_board, nuc11_board_path)


main = load_board

if __name__ == '__main__':
    test()
