<?php

namespace App\Services\PwHexParser;

use App\Services\PwHexParser\Contracts\Translate;
use App\Services\PwHexParser\Types\Unequippable;

class Decoder
{
    private int $position = 0;
    private string $hexString = '';
    private string $parsedHex = '';
    private string $lang = 'pt-BR';
    private int $nameLength = 0;
    private int $socketsCount = 0;
    private int $addonsCount = 0;
    private int $skillsCount = 0;
    private ?Translate $itemType = null;

    // Constants - These names are from original TS code but values refer to HEX STRING LENGTH (chars)
    // INT8_SIZE = 2 chars = 1 byte
    // INT32_SIZE = 4 chars = 2 bytes (short)
    // INT64_SIZE = 8 chars = 4 bytes (int32/float)
    private const INT8_SIZE = 2;
    private const INT32_SIZE = 4;
    private const INT64_SIZE = 8;
    private const MAX_SOCKETS_COUNT = 4;
    private const ATTACK_RATE_FACTOR = 20;
    private const SPECIAL_ADDON_ID = 4;
    private const SOCKET_ADDON_ID = 'a';
    private const DURABILITY_DIVIDER = 100;

    public function setHexString(string $hexString): void
    {
        $this->hexString = $hexString;
    }

    public function setLang(string $lang = 'pt-BR'): void
    {
        $this->lang = $lang;
        if ($this->itemType) {
            $this->itemType->setLang($lang);
        }
    }

    public function setItemType(string $itemType): void
    {
        $className = "App\\Services\\PwHexParser\\Types\\$itemType";
        if (!class_exists($className)) {
             $className = "App\\Services\\PwHexParser\\Types\\Unequippable";
        }

        $this->itemType = new $className();
        $this->itemType->setLang($this->lang);
    }
    
    public function getItemType(): ?Translate
    {
        return $this->itemType;
    }

    public function getPosition(): int
    {
        return $this->position;
    }

    private function validateHex(&$error): bool
    {
        if (empty($this->hexString)) {
            $error = "Hex string not set.";
            return false;
        }

        if (!ctype_xdigit($this->hexString)) {
            $error = "Invalid hexadecimal string.";
            return false;
        }

        if (!$this->itemType) {
            $error = "Item type not set.";
            return false;
        }

        if (strlen($this->hexString) < $this->itemType->getMinimumLength()) {
            $error = "Hex string is too short. Expected " . $this->itemType->getMinimumLength() . ", got " . strlen($this->hexString);
            return false;
        }

        return true;
    }

    public function decode(): array
    {
        if ($this->itemType instanceof Unequippable) {
            return [];
        }

        $error = '';
        if (!$this->validateHex($error)) {
            return ['error' => $error];
        }

        try {
            $structure = $this->itemType->getStructure();
            return $this->decodeStructure($structure);
        } catch (\Throwable $e) {
            return ['error' => $e->getMessage()];
        }
    }

    private function decodeStructure(array $structure): array
    {
        $result = [];

        foreach ($structure as $field => $type) {
            if (is_array($type)) {
                $count = $this->decodeType('', 'int64');
                $keys = $type;

                for ($i = 0; $i < $count; $i++) {
                    $item = [];
                    foreach ($keys as $key => $keyType) {
                         $item[$key] = $this->decodeType($key, $keyType);
                    }
                    $result[$field][] = $item;
                }
                continue;
            }

            $result[$field] = $this->decodeType($field, $type);
        }

        return $result;
    }

    public function debugHexString(): array
    {
        if ($this->itemType instanceof Unequippable) {
            return [];
        }

        $error = '';
        if (!$this->validateHex($error)) {
            return ['error' => "Error while validating hex for debug: $error"];
        }

        $result = [];
        $structure = $this->itemType->getStructure();

        foreach ($structure as $field => $type) {
             if (is_array($type)) {
                // Handle array type for debug
                // For simplicity in debug, just capture current pos and try to decode
                // This might be complex if we want structured debug for arrays
                // Let's just decode and show value
                 $result[$field] = [
                    'pos' => $this->position,
                    'value' => $this->decodeStructure([$field => $type])[$field], // Re-use logic? No, decodeStructure advances pos.
                    // But we are inside a loop that iterates structure.
                    // Wait, debugHexString in TS iterates structure and calls decodeType.
                    // Array logic is inside decodeStructure.
                    // So debugHexString in TS probably FAILS/is simple for arrays?
                    
                    // TS debugHexString implementation:
                    /*
                    for (const [field, type] of Object.entries(this.itemType!.getStructure())) {
                      result[field] = {
                        pos: this.position,
                        value: this.decodeType(field, type),
                        hex: this.parsedHex,
                      };
                    }
                    */
                    // TS decodeType handles basic types.
                    // BUT for arrays (like 'addons'), type is 'addons'.
                    // 'addons' IS a basic type in decodeType switch case.
                    // BUT 'skills' is also a basic type in decodeType switch case.
                    // BUT structure definition for some items might use ARRAY syntax (e.g. keys array).
                    // In `Genie`, 'skills' is mapped to 'skills' string (key in switch).
                    // So it's handled by decodeType.
                    
                    // BUT `getStructure` can return array for some fields?
                    // In `Decoder.php` decodeStructure:
                    /*
                    if (is_array($type)) {
                        // ... complex logic ...
                    }
                    */
                    // If structure has array value, TS `debugHexString` would fail or `decodeType` would receive array?
                    // TS `getStructure` returns `Record<string, string>`. So value is ALWAYS string.
                    // Wait. `Weapon.ts` structure: keys are strings.
                    // BUT `Decoder.ts` decodeStructure handles `Array.isArray(type)`.
                    // When does `type` become array?
                    // Maybe some types return object/array in structure?
                    // In my PHP generator, I assumed string.
                    // Let's check `Flight.ts` or others.
                    // They all return string values in `getStructure`.
                    // So `is_array($type)` in `decodeStructure` might be dead code or for future use?
                    // Or I missed something.
                    // Ah, `decodeStructure` in TS:
                    // for (const [field, type] of Object.entries(structure))
                    // if (Array.isArray(type)) ...
                    
                    // In TS `Record<string, string>` values are strings.
                    // Unless `structure` variable comes from somewhere else.
                    // `this.itemType!.getStructure()` returns Record<string, string>.
                    // So `type` is always string.
                    // So the Array check is indeed weird in TS, or the return type is looser than defined.
                
                 ];
                 continue;
             }
             
            $startPos = $this->position;
            $val = $this->decodeType($field, $type);
            $result[$field] = [
                'pos' => $startPos,
                'value' => $val,
                'hex' => $this->parsedHex, // parsedHex is updated by decodeType
            ];
        }

        return $result;
    }

    private function decodeType(string $field, string $type, int $prefixToRemove = 0)
    {
        switch ($type) {
            case 'addons_count':
                return $this->getAddonsCount($field);
            case 'addons':
                return $this->getAddons();
            case 'sockets_count':
                return $this->getSocketsCount($field);
            case 'sockets':
                return $this->getSockets();
            case 'name':
                return $this->getName();
            case 'pack_name':
                return $this->packName();
            case 'skills':
                return $this->getSkills($field);
            case 'skills_count':
                return $this->getSkillsCount($field);
            case 'skills_count_no_skip':
                 $this->skillsCount = $this->decodeType($field, 'int32');
                 return $this->skillsCount;
            case 'attack_rate':
                return $this->getAttackRate($field);
            case 'name_length':
                return $this->getNameLength($field);
            case 'durability':
                return $this->getDurability($field);
            case 'int32':
                $hex = substr($this->hexString, $this->position, self::INT32_SIZE);
                $this->parsedHex = $hex;
                $this->position += self::INT32_SIZE;
                return $this->hexToDecimal($hex, self::INT32_SIZE, $prefixToRemove, true);
            case 'int64':
                $hex = substr($this->hexString, $this->position, self::INT64_SIZE);
                $this->parsedHex = $hex;
                $this->position += self::INT64_SIZE;
                return $this->hexToDecimal($hex, self::INT64_SIZE, $prefixToRemove, true);
            case 'short':
                $hex = substr($this->hexString, $this->position, self::INT8_SIZE);
                $this->parsedHex = $hex;
                $val = $this->hexToDecimal($hex, self::INT8_SIZE, $prefixToRemove, true);
                $this->position += self::INT8_SIZE;
                return $val;
            case 'float':
                $hex = substr($this->hexString, $this->position, self::INT64_SIZE);
                $this->parsedHex = $hex;
                $this->position += self::INT64_SIZE;
                return $this->toFloat($hex);
            default:
                // Handle 'skip:N' type
                if (strpos($type, 'skip:') === 0) {
                    $count = (int)substr($type, 5);
                    $this->position += $count;
                    return null;
                }
                return 0;
        }
    }

    private function getSkillsCount(string $field): int
    {
        $this->skillsCount = $this->decodeType($field, 'int32');
        $this->position += (self::INT32_SIZE * 4);
        return $this->skillsCount;
    }

    private function getSkills(string $field): array
    {
        $skills = [];
        for ($i = 0; $i < $this->skillsCount; $i++) {
             $skillPos = $this->position + (self::INT32_SIZE * 4) + ($i * (self::INT32_SIZE * 4));
             
             $idHex = substr($this->hexString, $skillPos, self::INT64_SIZE);
             $levelHex = substr($this->hexString, $skillPos + self::INT64_SIZE, self::INT64_SIZE);
             
             $skills[] = [
                 'id' => $this->hexToDecimal($idHex, self::INT64_SIZE, 0, true),
                 'level' => $this->hexToDecimal($levelHex, self::INT64_SIZE, 0, true),
             ];
        }
        
        // Update position for consumed skills
        $this->position += $this->skillsCount * (self::INT64_SIZE * 2);
        
        return $skills;
    }

    private function getName(): string
    {
        $name = '';
        $nameLength = $this->nameLength / 2;
        
        for ($i = 0; $i < $nameLength; $i++) {
            $hex = substr($this->hexString, $this->position + $i * self::INT32_SIZE, 2);
            $charCode = $this->hexToDecimal($hex, 2, 0, true);
            $name .= chr($charCode);
        }
        
        $this->position += $nameLength * self::INT32_SIZE;
        return $name;
    }
    
    private function packName(): string
    {
        $name = '';
        $initialPos = $this->position + self::INT32_SIZE;
        
        for ($i = 0; $i < $this->nameLength; $i++) {
            $parsedHex = substr($this->hexString, $initialPos + $i * self::INT32_SIZE + self::INT8_SIZE, self::INT8_SIZE);
             $name .= hex2bin($parsedHex);
        }
        $this->position += self::INT8_SIZE;
        return $name;
    }

    private function getAddonsCount(string $field): int
    {
        $this->addonsCount = $this->decodeType($field, 'int64');
        if ($this->addonsCount > 30) {
            throw new \Exception("Invalid addons count: {$this->addonsCount}");
        }
        return $this->addonsCount;
    }

    private function getAddons(): array
    {
        $addons = [
            'special_addons' => [],
            'normal_addons' => [],
            'refine_addons' => [],
            'socket_addons' => [],
        ];

        if ($this->addonsCount <= 0) {
            return $addons;
        }

        $socketIndex = 0;
        $shift = 0;

        for ($i = 0; $i < $this->addonsCount; $i++) {
            $addonPos = $this->position + $i * (self::INT32_SIZE * 4) + $shift;
            
            $hexString = substr($this->hexString, $addonPos, self::INT64_SIZE);
            $hexString = $this->reverseHexNumber($hexString);
            $hexString = ltrim($hexString, '0');
            
            // if (strlen($hexString) % 2 !== 0) {
            //    continue;
            // }
            
            $hexString = trim($hexString);

            $addonType = substr($hexString, 0, 1);
            
            if ($addonType === (string)self::SPECIAL_ADDON_ID) {
                 $hexId = substr($hexString, 1);
                 $addonId = $this->hexToDecimal($hexId, self::INT64_SIZE, 0, false);
                 
                 $valHex = substr($this->hexString, $addonPos + self::INT64_SIZE, self::INT64_SIZE);
                 $lvlHex = substr($this->hexString, $addonPos + (self::INT32_SIZE * 4), self::INT64_SIZE);
                 
                 $addon = [
                     'id' => $addonId,
                     'value' => $this->hexToDecimal($valHex, self::INT64_SIZE, 0, true),
                     'level' => $this->hexToDecimal($lvlHex, self::INT64_SIZE, 0, true),
                 ];
                 
                 // Only first special addon (index 0) goes to special_addons if not in refine range
                 // Subsequent ones (index 1+) or those in refine range go to refine_addons
                 if ($addonId > 1691 && $addonId < 1892) {
                     $addons['refine_addons'][] = $addon;
                 } elseif (count($addons['special_addons']) === 0) {
                     $addons['special_addons'][] = $addon;
                 } else {
                     $addons['refine_addons'][] = $addon;
                 }
                 
                 $shift += self::INT64_SIZE;
            } elseif ($addonType === self::SOCKET_ADDON_ID) {
                $socketIndex++;
                $hexId = substr($hexString, 1);
                $addonId = $this->hexToDecimal($hexId, self::INT64_SIZE, 0, false);
                
                $valHex = substr($this->hexString, $addonPos + self::INT64_SIZE, self::INT64_SIZE);
                
                $socketAddon = [
                    'index' => $socketIndex,
                    'id' => $addonId,
                    'value' => $this->hexToDecimal($valHex, self::INT64_SIZE, 0, true),
                ];
                $addons['socket_addons'][] = $socketAddon;
            } else {
                $addonId = hexdec(substr($hexString, 1));
                $valHex = substr($this->hexString, $addonPos + self::INT64_SIZE, self::INT64_SIZE);
                
                $addon = [
                    'id' => $addonId,
                    'value' => $this->hexToDecimal($valHex, self::INT64_SIZE, 0, true),
                ];
                $addons['normal_addons'][] = $addon;
            }
        }
        
        // Update position to reflect consumed addons
        $this->position += ($this->addonsCount * (self::INT32_SIZE * 4)) + $shift;
        
        return $addons;
    }

    private function getSocketsCount(string $field): int
    {
        $this->socketsCount = $this->decodeType($field, 'int32');
        $this->position += self::INT32_SIZE;
        return $this->socketsCount;
    }

    private function getSockets(): array
    {
        $sockets = [];
        if ($this->socketsCount <= 0) {
            return $sockets;
        }
        
        if ($this->socketsCount > self::MAX_SOCKETS_COUNT) {
             throw new \Exception("Invalid sockets count: {$this->socketsCount}");
        }
        
        for ($i = 0; $i < $this->socketsCount; $i++) {
            $sockets[$i] = $this->decodeType('socket_'.$i, 'int64');
        }
        return $sockets;
    }

    private function getAttackRate(string $field)
    {
        $attackRate = $this->decodeType($field, 'int64');
        if ($attackRate <= 0) return 0;
        $rate = self::ATTACK_RATE_FACTOR / $attackRate;
        return round($rate * 100) / 100;
    }

    private function getNameLength(string $field): int
    {
        $this->nameLength = $this->decodeType($field, 'short');
        return $this->nameLength;
    }

    private function getDurability(string $field): int
    {
        $durability = $this->decodeType($field, 'int64');
        return (int)floor($durability / self::DURABILITY_DIVIDER);
    }

    private function reverseHexNumber(string $number): string
    {
        return implode('', array_reverse(str_split($number, 2)));
    }

    private function toFloat(string $hex): float
    {
        $bin = hex2bin($hex);
        $data = unpack('f', $bin);
        return $data[1];
    }

    private function hexToDecimal(string $hexString, int $expectedLength, $prefixToRemove, bool $reverse)
    {
        $paddingLength = $expectedLength - strlen($hexString);
        if ($paddingLength > 0) {
            $hexString = str_repeat('0', $paddingLength) . $hexString;
        }

        if ($reverse) {
            $hexString = $this->reverseHexNumber($hexString);
        }

        if (!ctype_xdigit($hexString)) {
            return 0;
        }

        if ($prefixToRemove !== null && $prefixToRemove !== 0) {
             $prefixStr = (string)$prefixToRemove;
             if (strpos($hexString, $prefixStr) === 0) {
                 $hexString = substr($hexString, strlen($prefixStr));
             }
        }

        return hexdec($hexString);
    }
    
    public function reset(): void
    {
        $this->position = 0;
        $this->hexString = '';
        $this->parsedHex = '';
        $this->nameLength = 0;
        $this->socketsCount = 0;
        $this->addonsCount = 0;
        $this->skillsCount = 0;
    }
}
