#  Copyright (C) 2021
#  ABM, Moscow
#
#  UNPUBLISHED PROPRIETARY MATERIAL.
#  ALL RIGHTS RESERVED.
#
#  Authors: Mike Orlov <m.orlov@abm-jsc.ru>
#
import collections.abc
import dataclasses
import types
import typing
from dataclasses import dataclass
from types import UnionType, NoneType
from typing import Union, Any

from .dict_to_dataclass import get_dataclass_field_name_to_field


@dataclass(slots=True, frozen=True)
class FieldHint:
    name: str
    type: type | types.UnionType | dataclasses.InitVar  # | typing.Union
    default: Any = dataclasses.field(default_factory=lambda: dataclasses.MISSING)

    def __eq__(self, other: 'FieldHint') -> bool:
        # InitVar[str] != InitVar[str], so make distinct type check
        if (not isinstance(other, FieldHint) or self.name != other.name or
                type(self.type) != type(self.type) or self.default != other.default):
            return False
        if isinstance(self.type, dataclasses.InitVar):
            return self.type.type == other.type.type
        return self.type == other.type

    def __repr__(self) -> str:
        parts = []
        parts += [f'name={self.name!r}']
        parts += [f'type={self.type.__name__ if isinstance(self.type, type) else self.type}']
        parts += [] if self.default is dataclasses.MISSING else [f'default={self.default!r}']
        return f'{type(self).__name__}({", ".join(parts)})'

    __str__ = __repr__


@dataclass(slots=True, frozen=True)
class StructureHint:
    fields: collections.abc.Sequence['FieldHint | StructureHint']
    name: str | None = None

    def __post_init__(self):
        object.__setattr__(self, 'fields', tuple(self.fields))

    @staticmethod
    def from_dataclass(dataclass_type: type) -> 'StructureHint':
        return dataclass_to_hint(dataclass_type)

    def __repr__(self) -> str:
        parts = []
        parts += [] if self.name is None else [f'name={self.name!r}']
        parts += [f'fields={list(self.fields)}']
        return f'{type(self).__name__}({", ".join(parts)})'

    def to_ini(self) -> str:
        return hint_to_ini(self)


def get_name_from_type(type_: type) -> str:
    type_origin = typing.get_origin(type_)
    type_args = typing.get_args(type_)
    if type_origin is not None and type_args is not None:
        if type_origin in (Union, UnionType):
            return ' | '.join(list(get_name_from_type(sub_type) for sub_type in type_args))
        result = get_name_from_type(type_origin)
        if type_args:
            result += '[' + (', '.join(list(get_name_from_type(sub_type) for sub_type in type_args))) + ']'
        return result
    elif isinstance(type_, dataclasses.InitVar):
        return get_name_from_type(type_.type)
    elif type_ in (None, NoneType):
        return "None"
    elif type_ is Ellipsis:
        return "..."
    return type_.__name__


def hint_to_ini(hint: StructureHint, depth: int = 0) -> str:
    section_name_to_lines: dict[str, list[str]] = {}
    section: list[str] | None = None
    for f in hint.fields:
        # if res:
        #     res += "\n"
        if isinstance(f, FieldHint):
            section = section or section_name_to_lines.setdefault('', [])
            default = "" if f.default is dataclasses.MISSING else f", default = {f.default}"
            section.append(f'; {f.name}: {get_name_from_type(f.type)}{default}')
            section.append(f'{f.name} = ...')
        elif isinstance(f, StructureHint):
            if depth:
                raise ValueError(f"INI does not support more than one nesting level, got at least two: {f.name}")
            section_name_to_lines[f.name] = [hint_to_ini(f, depth+1)]
        else:
            raise TypeError(f"unexpected type: {type(f)}")

    blocks = []
    for section_name, lines in section_name_to_lines.items():
        result = ''
        if section_name:
            result += f'[{section_name}]\n'
        result += '\n'.join(lines)
        blocks.append(result)
    return '\n\n'.join(blocks)


def dataclass_to_hint(dataclass_type: type) -> StructureHint:
    if not dataclasses.is_dataclass(dataclass_type):
        raise TypeError("expected dataclass type")

    args = []
    dataclass_fields = get_dataclass_field_name_to_field(dataclass_type)
    for field_name in dataclass_fields:
        field = dataclass_fields[field_name]
        if not field.init:
            continue

        default = dataclasses.MISSING
        if field.default != dataclasses.MISSING:
            default = field.default
        elif field.default_factory != dataclasses.MISSING:
            default = field.default_factory

        field_type = field.type
        if dataclasses.is_dataclass(field_type):
            hint = dataclass_to_hint(field_type)
            hint = dataclasses.replace(hint, name=field_name)
        else:
            hint = FieldHint(field_name, field_type, default)

        args.append(hint)
    return StructureHint(args)
