<?php

namespace Shm\ShmRPC\ShmRPCCodeGen;

use Shm\ShmTypes\ArrayOfType;
use Shm\ShmTypes\BaseType;
use Shm\ShmTypes\BoolType;
use Shm\ShmTypes\CompositeTypes\FileTypes\FileIDType;
use Shm\ShmTypes\EnumType;
use Shm\ShmTypes\FloatType;
use Shm\ShmTypes\IDsType;
use Shm\ShmTypes\IntType;
use Shm\ShmTypes\MixedType;
use Shm\ShmTypes\SelfRefType;
use Shm\ShmTypes\StaticType;
use Shm\ShmTypes\StructureType;
use Shm\ShmTypes\UnixDateTimeType;
use Shm\ShmTypes\UnixDateType;

class MarkdownTypeRenderer
{
    /** @var array<string, string> */
    private static array $structures = [];

    /** @var array<string, bool> */
    private static array $building = [];

    public static function reset(): void
    {
        self::$structures = [];
        self::$building = [];
    }

    public static function anchor(string $name): string
    {
        return strtolower($name);
    }

    public static function typeLabel(BaseType $type, bool $input = false): string
    {
        if ($type instanceof SelfRefType) {
            return self::typeLabel($type->resolveType(), $input);
        }

        if ($type instanceof FileIDType) {
            return self::typeLabel($type->getDocument(), $input);
        }

        if ($type instanceof StructureType) {
            $name = $input ? $type->inputTypeName() : $type->typeName();
            self::registerStructure($type, $input, $name);

            return '[' . $name . '](#' . self::anchor($name) . ')';
        }

        if ($type instanceof StaticType) {
            $value = $type->normalize(null);
            if (is_string($value)) {
                return '`"' . addslashes($value) . '"`';
            }

            return '`' . json_encode($value, JSON_UNESCAPED_UNICODE) . '`';
        }

        if ($type instanceof EnumType) {
            $values = array_map(fn($v) => '`' . $v . '`', array_keys($type->values));

            return 'enum: ' . implode(' \\| ', $values);
        }

        if ($type instanceof ArrayOfType) {
            return self::typeLabel($type->itemType, $input) . '[]';
        }

        if ($type instanceof IDsType) {
            return 'string[]';
        }

        if ($type instanceof MixedType) {
            return 'mixed';
        }

        if ($type instanceof BoolType) {
            return 'boolean';
        }

        if ($type instanceof IntType || $type instanceof UnixDateType || $type instanceof UnixDateTimeType) {
            return 'integer';
        }

        if ($type instanceof FloatType) {
            return 'number';
        }

        return 'string';
    }

    public static function typeNamePlain(BaseType $type, bool $input = false): string
    {
        if ($type instanceof SelfRefType) {
            return self::typeNamePlain($type->resolveType(), $input);
        }

        if ($type instanceof FileIDType) {
            return self::typeNamePlain($type->getDocument(), $input);
        }

        if ($type instanceof StructureType) {
            return $input ? $type->inputTypeName() : $type->typeName();
        }

        if ($type instanceof StaticType) {
            return 'static';
        }

        if ($type instanceof EnumType) {
            return 'enum';
        }

        if ($type instanceof ArrayOfType) {
            return self::typeNamePlain($type->itemType, $input) . '[]';
        }

        if ($type instanceof IDsType) {
            return 'string[]';
        }

        if ($type instanceof MixedType) {
            return 'mixed';
        }

        if ($type instanceof BoolType) {
            return 'boolean';
        }

        if ($type instanceof IntType || $type instanceof UnixDateType || $type instanceof UnixDateTimeType) {
            return 'integer';
        }

        if ($type instanceof FloatType) {
            return 'number';
        }

        return 'string';
    }

    public static function structureTable(StructureType $type, bool $input = false): string
    {
        $name = $input ? $type->inputTypeName() : $type->typeName();
        self::registerStructure($type, $input, $name);

        $rows = [];
        $hasAdditional = false;
        $additionalLabel = '';

        foreach ($type->items as $key => $item) {
            if ($item->hide) {
                continue;
            }

            if ($input && (!$item->editable || $item instanceof StaticType)) {
                continue;
            }

            if ($key === '*') {
                $hasAdditional = true;
                $additionalLabel = self::typeLabel($item, $input);
                continue;
            }

            $rows[] = self::formatTableRow((string) $key, $item, $input);
        }

        if ($rows === [] && !$hasAdditional) {
            return '_Нет полей_';
        }

        $table = "| Поле | Тип | Обязательный | Описание |\n|------|-----|--------------|----------|\n";
        $table .= implode("\n", $rows);

        if ($hasAdditional) {
            $table .= "\n\n_Дополнительные поля (`[key: string]`):_ " . $additionalLabel;
        }

        return $table;
    }

    public static function getTypesSection(): string
    {
        if (self::$structures === []) {
            return '';
        }

        ksort(self::$structures);

        $parts = ["\n\n## Типы\n"];
        foreach (self::$structures as $name => $content) {
            $parts[] = $content;
        }

        return implode("\n\n", $parts);
    }

    /**
     * @return mixed
     */
    public static function exampleValue(BaseType $type, bool $input = false, bool $includeNullable = false)
    {
        if ($type->nullable && !$includeNullable) {
            return null;
        }

        if ($type instanceof SelfRefType) {
            return self::exampleValue($type->resolveType(), $input, $includeNullable);
        }

        if ($type instanceof FileIDType) {
            return self::exampleValue($type->getDocument(), $input, $includeNullable);
        }

        if ($type instanceof StructureType) {
            $result = [];
            foreach ($type->items as $key => $item) {
                if ($item->hide) {
                    continue;
                }

                if ($input && (!$item->editable || $item instanceof StaticType)) {
                    continue;
                }

                if ($key === '*') {
                    continue;
                }

                if ($includeNullable || !$item->nullable) {
                    $result[$key] = self::exampleValue($item, $input, $includeNullable);
                }
            }

            return $result;
        }

        if ($type instanceof StaticType) {
            return $type->normalize(null);
        }

        if ($type instanceof EnumType) {
            $keys = array_keys($type->values);

            return $keys[0] ?? '';
        }

        if ($type instanceof ArrayOfType || $type instanceof IDsType) {
            return [];
        }

        if ($type instanceof BoolType) {
            return false;
        }

        if ($type instanceof IntType || $type instanceof UnixDateType || $type instanceof UnixDateTimeType) {
            return 0;
        }

        if ($type instanceof FloatType) {
            return 0.0;
        }

        if ($type instanceof MixedType) {
            return null;
        }

        return '';
    }

    private static function registerStructure(StructureType $type, bool $input, string $name): void
    {
        if (isset(self::$structures[$name])) {
            return;
        }

        if (isset(self::$building[$name])) {
            return;
        }

        self::$building[$name] = true;

        $title = $type->title ? ' — ' . self::escapeMd($type->title) : '';
        $section = '### ' . $name . ' {#' . self::anchor($name) . '}' . $title . "\n\n";
        $section .= self::buildStructureTableContent($type, $input);

        self::$structures[$name] = $section;
        unset(self::$building[$name]);
    }

    private static function buildStructureTableContent(StructureType $type, bool $input): string
    {
        $rows = [];
        $hasAdditional = false;
        $additionalLabel = '';

        foreach ($type->items as $key => $item) {
            if ($item->hide) {
                continue;
            }

            if ($input && (!$item->editable || $item instanceof StaticType)) {
                continue;
            }

            if ($key === '*') {
                $hasAdditional = true;
                $additionalLabel = self::typeLabel($item, $input);
                continue;
            }

            $rows[] = self::formatTableRow((string) $key, $item, $input);
        }

        if ($rows === [] && !$hasAdditional) {
            return '_Нет полей_';
        }

        $table = "| Поле | Тип | Обязательный | Описание |\n|------|-----|--------------|----------|\n";
        $table .= implode("\n", $rows);

        if ($hasAdditional) {
            $table .= "\n\n_Дополнительные поля (`[key: string]`):_ " . $additionalLabel;
        }

        return $table;
    }

    private static function formatTableRow(string $key, BaseType $item, bool $input): string
    {
        $typeLabel = self::typeLabel($item, $input);
        $required = $item->nullable ? 'нет' : 'да';
        $description = self::fieldDescription($item);

        return '| `' . self::escapeMd($key) . '` | ' . $typeLabel . ' | ' . $required . ' | ' . $description . ' |';
    }

    private static function fieldDescription(BaseType $item): string
    {
        $parts = [];

        if ($item->title) {
            $parts[] = self::escapeMd($item->title);
        }

        if ($item->description) {
            $parts[] = self::escapeMd($item->description);
        }

        if ($item->deprecated) {
            $parts[] = '**deprecated:** ' . self::escapeMd($item->deprecated);
        }

        return $parts !== [] ? implode('. ', $parts) : '—';
    }

    private static function escapeMd(string $text): string
    {
        return str_replace(['|', "\n"], ['\\|', ' '], $text);
    }
}
