from pathlib import Path
import logging

from parser import ExtractedSymbolType
from generators.base_generator import BaseGenerator

logger = logging.getLogger(__name__)

def get_common_prefix(keys: list[str]) -> str:
    # find a prefix that's shared between all keys
    prefix = ""
    for i in range(min(map(len, keys))):
        if len(set(key[i] for key in keys)) > 1:
            break
        prefix += keys[0][i]
    return prefix

def snake_case_to_pascal_case(snake_case: str) -> str:
    return ''.join(word.lower().capitalize() for word in snake_case.split('_'))


SYMBOL_NAME_OVERRIDES = {
    'Clay_TextElementConfigWrapMode': 'TextWrapMode',
    'Clay_Border': 'BorderData',
    'Clay_SizingMinMax': 'SizingConstraintsMinMax',
}
SYMBOL_COMPLETE_OVERRIDES = {
    'Clay_RenderCommandArray': 'ClayArray(RenderCommand)',
    'Clay_Context': 'Context',
    'Clay_ElementConfig': None,
    # 'Clay_SetQueryScrollOffsetFunction': None,
}

# These enums should have output binding members that are PascalCase instead of UPPER_SNAKE_CASE.
ENUM_MEMBER_PASCAL = {
    'Clay_RenderCommandType',
    'Clay_TextElementConfigWrapMode',
    'Clay__ElementConfigType',
}
ENUM_MEMBER_OVERRIDES = {
    'Clay__ElementConfigType': {
        'CLAY__ELEMENT_CONFIG_TYPE_BORDER_CONTAINER': 'Border',
        'CLAY__ELEMENT_CONFIG_TYPE_FLOATING_CONTAINER': 'Floating',
        'CLAY__ELEMENT_CONFIG_TYPE_SCROLL_CONTAINER': 'Scroll',
    }
}
ENUM_ADDITIONAL_MEMBERS = {
    'Clay__ElementConfigType': {
        'Id': 65,
        'Layout': 66,
    }
}

TYPE_MAPPING = {
    '*char': '[^]c.char',
    'const *char': '[^]c.char',
    '*void': 'rawptr',
    'bool': 'bool',
    'float': 'c.float',
    'uint16_t': 'u16',
    'uint32_t': 'u32',
    'int32_t': 'c.int32_t',
    'uintptr_t': 'rawptr',
    'intptr_t': 'rawptr',
    'void': 'void',
}
STRUCT_TYPE_OVERRIDES = {
    'Clay_Arena': {
        'nextAllocation': 'uintptr',
        'capacity': 'uintptr',
    },
    'Clay_SizingAxis': {
        'size': 'SizingConstraints',
    },
    "Clay_RenderCommand": {
        "zIndex": 'i32',
    },
}
STRUCT_MEMBER_OVERRIDES = {
    'Clay_ErrorHandler': {
        'errorHandlerFunction': 'handler',
    },
    'Clay_SizingAxis': {
        'size': 'constraints',
    },
}
STRUCT_OVERRIDE_AS_FIXED_ARRAY = {
    'Clay_Color',
    'Clay_Vector2',
}

FUNCTION_PARAM_OVERRIDES = {
    'Clay_SetCurrentContext': {
        'context': 'ctx',
    },
}
FUNCTION_TYPE_OVERRIDES = {
    'Clay_CreateArenaWithCapacityAndMemory': {
        'offset': '[^]u8',
    },
    'Clay_SetMeasureTextFunction': {
        'userData': 'uintptr',
    },
    'Clay_RenderCommandArray_Get': {
        'index': 'i32',
    },
    "Clay__AttachElementConfig": {
        "config": 'rawptr',
    },
}

class OdinGenerator(BaseGenerator):

    def generate(self) -> None:
        self.generate_structs()
        self.generate_enums()
        self.generate_functions()

        odin_template_path = Path(__file__).parent / 'odin' / 'clay.template.odin'
        with open(odin_template_path, 'r') as f:
            template = f.read()
        self.output_content['clay.odin'] = (
            template
            .replace('{{structs}}', '\n'.join(self.output_content['struct']))
            .replace('{{enums}}', '\n'.join(self.output_content['enum']))
            .replace('{{public_functions}}', '\n'.join(self.output_content['public_function']))
            .replace('{{private_functions}}', '\n'.join(self.output_content['private_function']))
            .splitlines()
        )
        del self.output_content['struct']
        del self.output_content['enum']
        del self.output_content['private_function']
        del self.output_content['public_function']

    def get_symbol_name(self, symbol: str) -> str:
        if symbol in SYMBOL_NAME_OVERRIDES:
            return SYMBOL_NAME_OVERRIDES[symbol]
        symbol_type = self.get_symbol_type(symbol)
        base_name = symbol.removeprefix('Clay_')
        if symbol_type == 'enum':
            return base_name.removeprefix('_') # Clay_ and Clay__ are exported as public types.
        elif symbol_type == 'struct':
            return base_name
        elif symbol_type == 'function':
            return base_name
        raise ValueError(f'Unknown symbol: {symbol}')
    
    def format_type(self, type: ExtractedSymbolType) -> str:
        if isinstance(type, str):
            return type
        
        parameter_strs = []
        for param_name, param_type in type['params']:
            parameter_strs.append(f"{param_name}: {self.format_type(param_type or 'unknown')}")
        return_type_str = ''
        if type['return_type'] is not None and type['return_type'] != 'void':
            return_type_str = ' -> ' + self.format_type(type['return_type'])
        return f"proc \"c\" ({', '.join(parameter_strs)}){return_type_str}"
    
    def resolve_binding_type(self, symbol: str, member: str | None, member_type: ExtractedSymbolType | None, type_overrides: dict[str, dict[str, str]]) -> str | None:
        if isinstance(member_type, str):
            if member_type in SYMBOL_COMPLETE_OVERRIDES:
                return SYMBOL_COMPLETE_OVERRIDES[member_type]
            if symbol in type_overrides and member in type_overrides[symbol]:
                return type_overrides[symbol][member]
            if member_type in TYPE_MAPPING:
                return TYPE_MAPPING[member_type]
            if member_type and self.has_symbol(member_type):
                return self.get_symbol_name(member_type)
            if member_type and member_type.startswith('*'):
                result = self.resolve_binding_type(symbol, member, member_type[1:], type_overrides)
                if result:
                    return f"^{result}"
            return None
        if member_type is None:
            return None

        resolved_parameters = []
        for param_name, param_type in member_type['params']:
            resolved_param = self.resolve_binding_type(symbol, param_name, param_type, type_overrides)
            if resolved_param is None:
                return None
            resolved_parameters.append((param_name, resolved_param))
        resolved_return_type = self.resolve_binding_type(symbol, None, member_type['return_type'], type_overrides)
        if resolved_return_type is None:
            return None
        return self.format_type({
            "params": resolved_parameters,
            "return_type": resolved_return_type,
        })

    def generate_structs(self) -> None:
        for struct, struct_data in sorted(self.extracted_symbols.structs.items(), key=lambda x: x[0]):
            members = struct_data['attrs']
            if not struct.startswith('Clay_'):
                continue
            if struct in SYMBOL_COMPLETE_OVERRIDES:
                continue

            binding_name = self.get_symbol_name(struct)
            if binding_name.startswith('_'):
                continue

            if struct in STRUCT_OVERRIDE_AS_FIXED_ARRAY:
                array_size = len(members)
                first_elem = list(members.values())[0]
                array_type = None
                if 'type' in first_elem:
                   array_type = first_elem['type']

                if array_type in TYPE_MAPPING:
                    array_binding_type = TYPE_MAPPING[array_type]
                elif array_type and self.has_symbol(self.format_type(array_type)):
                    array_binding_type = self.get_symbol_name(self.format_type(array_type))
                else:
                    self._write('struct', f"// {struct} ({array_type}) - has no mapping")
                    continue
                
                self._write('struct', f"// {struct} (overridden as fixed array)")
                self._write('struct', f"{binding_name} :: [{array_size}]{array_binding_type}")
                self._write('struct', "")
                continue

            raw_union = ' #raw_union' if struct_data.get('is_union', False) else ''

            self._write('struct', f"// {struct}")
            self._write('struct', f"{binding_name} :: struct{raw_union} {{")

            for member, member_info in members.items():
                if struct in STRUCT_TYPE_OVERRIDES and member in STRUCT_TYPE_OVERRIDES[struct]:
                    member_type = 'unknown'
                elif not 'type' in member_info:
                    self._write('struct', f"    // {member} (unknown type)")
                    continue
                else:
                    member_type = member_info['type']
                    
                binding_member_name = member
                if struct in STRUCT_MEMBER_OVERRIDES and member in STRUCT_MEMBER_OVERRIDES[struct]:
                    binding_member_name = STRUCT_MEMBER_OVERRIDES[struct][member]

                member_binding_type = self.resolve_binding_type(struct, member, member_type, STRUCT_TYPE_OVERRIDES)
                if member_binding_type is None:
                    self._write('struct', f"    // {binding_member_name} ({member_type}) - has no mapping")
                    continue
                self._write('struct', f"    {binding_member_name}: {member_binding_type}, // {member} ({member_type})")
            self._write('struct', "}")
            self._write('struct', '')

    def generate_enums(self) -> None:
        for enum, members in sorted(self.extracted_symbols.enums.items(), key=lambda x: x[0]):
            if not enum.startswith('Clay_'):
                continue
            if enum in SYMBOL_COMPLETE_OVERRIDES:
                continue

            binding_name = self.get_symbol_name(enum)
            common_member_prefix = get_common_prefix(list(members.keys()))
            self._write('enum', f"// {enum}")
            self._write('enum', f"{binding_name} :: enum EnumBackingType {{")
            for member in members:
                if enum in ENUM_MEMBER_OVERRIDES and member in ENUM_MEMBER_OVERRIDES[enum]:
                    binding_member_name = ENUM_MEMBER_OVERRIDES[enum][member]
                else:
                    binding_member_name = member.removeprefix(common_member_prefix)
                    if enum in ENUM_MEMBER_PASCAL:
                        binding_member_name = snake_case_to_pascal_case(binding_member_name)

                if members[member] is not None:
                    self._write('enum', f"    {binding_member_name} = {members[member]}, // {member}")
                else:
                    self._write('enum', f"    {binding_member_name}, // {member}")
                
            if enum in ENUM_ADDITIONAL_MEMBERS:
                self._write('enum', '    // Odin specific enum types')
                for member, value in ENUM_ADDITIONAL_MEMBERS[enum].items():
                    self._write('enum', f"    {member} = {value},")
            self._write('enum', "}")
            self._write('enum', '')

    def generate_functions(self) -> None:
        for function, function_info in sorted(self.extracted_symbols.functions.items(), key=lambda x: x[0]):
            if not function.startswith('Clay_'):
                continue
            if function in SYMBOL_COMPLETE_OVERRIDES:
                continue
            is_private = function.startswith('Clay__')
            write_to = 'private_function' if is_private else 'public_function'

            binding_name = self.get_symbol_name(function)

            return_type = function_info['return_type']
            binding_return_type = self.resolve_binding_type(function, None, return_type, {})
            if binding_return_type is None:
                self._write(write_to, f"    // {function} ({return_type}) - has no mapping")
                continue

            skip = False
            binding_params = []
            for param_name, param_type in function_info['params']:
                binding_param_name = param_name
                if function in FUNCTION_PARAM_OVERRIDES and param_name in FUNCTION_PARAM_OVERRIDES[function]:
                    binding_param_name = FUNCTION_PARAM_OVERRIDES[function][param_name]
                binding_param_type = self.resolve_binding_type(function, param_name, param_type, FUNCTION_TYPE_OVERRIDES)
                if binding_param_type is None:
                    skip = True
                binding_params.append(f"{binding_param_name}: {binding_param_type}")
            if skip:
                self._write(write_to, f"    // {function} - has no mapping")
                continue

            binding_params_str = ', '.join(binding_params)
            return_str = f" -> {binding_return_type}" if binding_return_type != 'void' else ''
            self._write(write_to, f"    {binding_name} :: proc({binding_params_str}){return_str} --- // {function}")