"""
rbxl_xml_parser.py — парсер XML-формата .rbxl (старые карты до 2010 года).

Roblox-XML формат — текстовый предок бинарного .rbxl. Файл начинается с
<roblox version="4"> и содержит дерево <Item class="...">...</Item>.

Возвращает тот же `RobloxModel` что и rbxl_parser.parse — чтобы converter.py
работал без изменений.

Пример входного файла:
    <roblox version="4">
        <Item class="Workspace">
            <Properties>
                <string name="Name">Workspace</string>
            </Properties>
            <Item class="Part">
                <Properties>
                    <CoordinateFrame name="CFrame">
                        <X>0</X><Y>10</Y><Z>0</Z>
                        <R00>1</R00>...<R22>1</R22>
                    </CoordinateFrame>
                    <Vector3 name="size"><X>4</X><Y>1</Y><Z>2</Z></Vector3>
                    <Color3uint8 name="Color3uint8">4286611584</Color3uint8>
                    <token name="BrickColor">21</token>
                </Properties>
            </Item>
        </Item>
    </roblox>

Поддерживает все типичные property-теги: string, bool, int, float, double,
token, Vector3, Vector2, CoordinateFrame, Color3, Color3uint8, BrickColor,
Content, ProtectedString, Ref, BinaryString, UDim, UDim2, Rect2D.
"""
from __future__ import annotations
from typing import Dict, List, Any, Optional, Tuple
import xml.etree.ElementTree as ET
import re
import base64
import struct

from rbxl_parser import Instance, RobloxModel
from rbxl_types import (
    Vector3, Vector2, Color3, CFrame, BrickColor,
    EnumValue, PhysicalProperties, OptionalCFrame,
)


# Magic для XML-формата
XML_MAGIC = b'<roblox'


def is_xml_rbxl(blob: bytes) -> bool:
    """Проверяет XML это или нет. Бинарный начинается с <roblox!..."""
    stripped = blob.lstrip()
    if stripped.startswith(b'<roblox') and not stripped.startswith(b'<roblox!'):
        return True
    return False


def _text(el: ET.Element, default: str = '') -> str:
    """Текст элемента (None → default)."""
    return (el.text if el.text is not None else default).strip()


def _f(el: ET.Element, default: float = 0.0) -> float:
    """Float из text."""
    try:
        return float(_text(el, '0'))
    except (ValueError, TypeError):
        return default


def _i(el: ET.Element, default: int = 0) -> int:
    """Int из text."""
    try:
        s = _text(el, '0')
        # Roblox иногда пишет '1.0' где ожидается int
        return int(float(s)) if '.' in s else int(s)
    except (ValueError, TypeError):
        return default


def _parse_vector3(el: ET.Element) -> Vector3:
    x = _f(el.find('X'))
    y = _f(el.find('Y'))
    z = _f(el.find('Z'))
    return Vector3(x, y, z)


def _parse_vector2(el: ET.Element) -> Vector2:
    x = _f(el.find('X'))
    y = _f(el.find('Y'))
    return Vector2(x, y)


def _parse_cframe(el: ET.Element) -> CFrame:
    """CoordinateFrame: 3 позиции + 9 элементов матрицы ротации."""
    pos = Vector3(_f(el.find('X')), _f(el.find('Y')), _f(el.find('Z')))
    matrix = tuple(_f(el.find(f'R{i}{j}'), 1.0 if i == j else 0.0)
                   for i in range(3) for j in range(3))
    return CFrame(position=pos, matrix=matrix)


def _parse_color3(el: ET.Element) -> Color3:
    """<Color3 name="..."><R>...</R><G>...</G><B>...</B></Color3>"""
    r = _f(el.find('R'))
    g = _f(el.find('G'))
    b = _f(el.find('B'))
    return Color3(r, g, b)


def _parse_color3uint8(el: ET.Element) -> Color3:
    """<Color3uint8>4286611584</Color3uint8> — packed RGB как uint32."""
    val = _i(el, 0)
    # uint32 = 0xFFRRGGBB (alpha=FF). r=byte2, g=byte1, b=byte0
    b = (val & 0xff) / 255.0
    g = ((val >> 8) & 0xff) / 255.0
    r = ((val >> 16) & 0xff) / 255.0
    return Color3(r, g, b)


def _parse_property(prop_el: ET.Element) -> Tuple[str, Any]:
    """Парсит один <тип name="имя">значение</тип>. Возвращает (name, value)."""
    tag = prop_el.tag
    name = prop_el.attrib.get('name', '')

    if tag == 'string' or tag == 'ProtectedString' or tag == 'Content':
        return name, _text(prop_el)

    if tag == 'bool':
        return name, _text(prop_el).lower() == 'true'

    if tag in ('int', 'int64'):
        val = _i(prop_el)
        # В старом XML цвет хранится как <int name="BrickColor">21</int>,
        # а converter ожидает BrickColor-объект с .code.
        if name == 'BrickColor':
            return name, BrickColor(code=val)
        return name, val

    if tag in ('float', 'double'):
        return name, _f(prop_el)

    if tag == 'token':
        # token — int-значение enum
        return name, EnumValue(value=_i(prop_el))

    if tag == 'Vector3':
        return name, _parse_vector3(prop_el)

    if tag == 'Vector2':
        return name, _parse_vector2(prop_el)

    if tag == 'CoordinateFrame':
        return name, _parse_cframe(prop_el)

    if tag == 'Color3':
        return name, _parse_color3(prop_el)

    if tag == 'Color3uint8':
        return name, _parse_color3uint8(prop_el)

    if tag == 'BrickColor':
        return name, BrickColor(code=_i(prop_el))

    if tag == 'Ref':
        # Ссылка на другой Item по referent (например "RBX42" или "null")
        txt = _text(prop_el, 'null')
        if txt in ('null', 'nil', ''):
            return name, None
        return name, txt  # храним как строку-referent

    if tag == 'BinaryString':
        # base64 → bytes
        try:
            return name, base64.b64decode(_text(prop_el))
        except Exception:
            return name, b''

    if tag == 'UDim':
        scale = _f(prop_el.find('S'))
        offset = _i(prop_el.find('O'))
        return name, {'scale': scale, 'offset': offset}

    if tag == 'UDim2':
        xs = _f(prop_el.find('XS'))
        xo = _i(prop_el.find('XO'))
        ys = _f(prop_el.find('YS'))
        yo = _i(prop_el.find('YO'))
        return name, {'x_scale': xs, 'x_offset': xo, 'y_scale': ys, 'y_offset': yo}

    if tag == 'Rect2D':
        # min/max
        min_el = prop_el.find('min')
        max_el = prop_el.find('max')
        return name, {
            'min': _parse_vector2(min_el) if min_el is not None else Vector2(0, 0),
            'max': _parse_vector2(max_el) if max_el is not None else Vector2(0, 0),
        }

    if tag == 'OptionalCoordinateFrame':
        cf_el = prop_el.find('CFrame')
        return name, OptionalCFrame(cframe=_parse_cframe(cf_el)) if cf_el is not None else OptionalCFrame(cframe=None)

    if tag == 'PhysicalProperties':
        cust = prop_el.find('CustomPhysics')
        custom = cust is not None and _text(cust).lower() == 'true'
        return name, PhysicalProperties(
            custom_physics=custom,
            density=_f(prop_el.find('Density'), 0.7),
            friction=_f(prop_el.find('Friction'), 0.3),
            elasticity=_f(prop_el.find('Elasticity'), 0.5),
            friction_weight=_f(prop_el.find('FrictionWeight'), 1.0),
            elasticity_weight=_f(prop_el.find('ElasticityWeight'), 1.0),
        )

    if tag == 'NumberRange':
        return name, {'min': _f(prop_el.find('Min')), 'max': _f(prop_el.find('Max'))}

    # SharedString / Uri / другие незнакомые — оставляем как текст
    return name, _text(prop_el)


# Регекс для извлечения referent из строк типа "RBX42"
_REF_RE = re.compile(r'^RBX(\d+)$')


def _ref_to_int(ref: Optional[str]) -> Optional[int]:
    """RBX42 → 42, null → None. Если уникальной номер не найден — None."""
    if ref is None or ref == 'null':
        return None
    m = _REF_RE.match(str(ref))
    if m:
        return int(m.group(1))
    return None


def parse_xml(blob: bytes) -> RobloxModel:
    """Главный entry: bytes → RobloxModel."""
    try:
        text = blob.decode('utf-8', errors='replace')
    except Exception:
        text = blob.decode('latin-1', errors='replace')

    # XML может иметь BOM или leading whitespace
    text = text.lstrip('﻿').lstrip()

    root = ET.fromstring(text)

    instances: List[Instance] = []
    by_referent: Dict[int, Instance] = {}
    roots: List[Instance] = []

    # Auto-increment id для Item'ов без referent (старые форматы)
    next_id_counter = [100000]

    def _walk(item_el: ET.Element, parent_ref: Optional[int]) -> None:
        """Рекурсивный обход <Item class="..."> элементов."""
        cls = item_el.attrib.get('class', 'Unknown')

        # Referent из атрибута (например referent="RBX42")
        ref_attr = item_el.attrib.get('referent') or item_el.attrib.get('Referent')
        ref_int = _ref_to_int(ref_attr) if ref_attr else None
        if ref_int is None:
            # Назначаем auto-id чтобы converter мог отслеживать parent_referent
            ref_int = next_id_counter[0]
            next_id_counter[0] += 1

        # Парсим properties
        props: Dict[str, Any] = {}
        props_el = item_el.find('Properties')
        if props_el is not None:
            for prop_el in props_el:
                try:
                    pname, pval = _parse_property(prop_el)
                    if pname:
                        props[pname] = pval
                except Exception:
                    continue

        # Roblox в старых картах использовал имена с маленькой первой буквы:
        # name → Name, size → Size, shape → Shape, и т.д. Converter ожидает
        # PascalCase. Делаем алиасы (старое имя остаётся, новое добавляется).
        _ALIAS_TO_PASCAL = {
            'name': 'Name',
            'size': 'Size',
            'shape': 'Shape',
            'archivable': 'Archivable',
            'shape3d': 'Shape',
        }
        for old, new in _ALIAS_TO_PASCAL.items():
            if old in props and new not in props:
                props[new] = props[old]

        # Convert Ref-properties (string "RBX42") в parent_referent если нужно
        # — пока оставляем как строки.

        inst = Instance(
            referent=ref_int,
            class_name=cls,
            properties=props,
            parent_referent=parent_ref,
            children=[],
        )
        instances.append(inst)
        by_referent[ref_int] = inst
        if parent_ref is None:
            roots.append(inst)

        # Рекурсивно дочерние Item'ы
        for child in item_el.findall('Item'):
            _walk(child, ref_int)

    # Roblox-XML: top-level <Item class="..."> идут под <roblox>
    for item in root.findall('Item'):
        _walk(item, None)

    # Заполняем children после полного прохода (для удобства converter'а)
    for inst in instances:
        if inst.parent_referent is not None:
            parent = by_referent.get(inst.parent_referent)
            if parent is not None:
                parent.children.append(inst)

    # Версия из атрибута <roblox version="4">
    version_attr = root.attrib.get('version', '4')
    try:
        version = int(version_attr)
    except ValueError:
        version = 4

    return RobloxModel(
        version=version,
        class_count=len(set(i.class_name for i in instances)),
        instance_count=len(instances),
        instances=instances,
        by_referent=by_referent,
        roots=roots,
        shared_strings=[],
        meta={},
        warnings=[],
    )
