import collections
import dataclasses
import os
import pathlib
from typing import Dict
from typing import List
from typing import Optional

import jinja2

from chaotic import cpp_format
from chaotic import jinja_env
from chaotic.back.cpp import types as cpp_types
from chaotic.front import types

PARENT_DIR = os.path.dirname(__file__)


@dataclasses.dataclass
class CppOutputFile:
    content: str
    ext: str
    subdir: str


@dataclasses.dataclass
class CppOutput:
    filepath_wo_ext: str
    files: List[CppOutputFile]


current_namespace = ''  # pylint: disable=invalid-name


def get_current_namespace() -> str:
    return current_namespace


def close_namespace() -> str:
    if current_namespace:
        data = ''
        for name in reversed(current_namespace.split('::')):
            data += '} //' + name + '\n'
        return data
    else:
        return ''


def open_namespace(new_ns: str) -> str:
    # pylint: disable=global-statement
    global current_namespace
    current_namespace = new_ns

    if new_ns:
        res = ''
        for namespace in new_ns.split('::'):
            res = res + f'namespace {namespace} {{'
        return res
    else:
        return ''


def cpp_namespace(name: str) -> str:
    if '::' not in name:
        return ''
    ns_part, _name_part = name.rsplit('::', 1)
    return ns_part


def cpp_type(name: str) -> str:
    if '::' not in name:
        return name
    _ns_part, name_part = name.rsplit('::', 1)
    return name_part


def declaration_includes(types: List[cpp_types.CppType]) -> List[str]:
    includes = set()
    for type_ in types:
        includes.update(set(type_.declaration_includes()))
    return sorted(includes)


def definition_includes(types: List[cpp_types.CppType]) -> List[str]:
    includes = set()
    for type_ in types:
        includes.update(set(type_.definition_includes()))
    return sorted(includes)


def extra_cpp_type(type_: cpp_types.CppStruct) -> str:
    extra_type = type_.extra_type
    if extra_type is True:
        return 'USERVER_NAMESPACE::formats::json::Value'
    else:
        assert isinstance(extra_type, cpp_types.CppType)
        container = type_.extra_container()
        return f'{container}<std::string, {extra_type.cpp_user_name()}>'


def extra_cpp_parser_type(extra_type: cpp_types.CppType) -> str:
    assert isinstance(extra_type, cpp_types.CppType)
    return extra_type.parser_type(ns='TODO', name='Extra')


def cpp_struct_is_strict_parsing(struct: cpp_types.CppStruct) -> bool:
    assert isinstance(struct, cpp_types.CppStruct)
    return struct.strict_parsing and struct.extra_type is False


def make_env() -> jinja2.Environment:
    env = jinja_env.make_env(
        'chaotic/chaotic/back/cpp', os.path.join(PARENT_DIR),
    )

    env.globals['enumerate'] = enumerate

    env.globals['cpp_struct_is_strict_parsing'] = cpp_struct_is_strict_parsing

    env.globals['declaration_includes'] = declaration_includes
    env.globals['definition_includes'] = definition_includes

    env.globals['cpp_namespace'] = cpp_namespace
    env.globals['cpp_type'] = cpp_type

    env.globals['extra_cpp_type'] = extra_cpp_type
    env.globals['extra_cpp_parser_type'] = extra_cpp_parser_type

    env.globals['open_namespace'] = open_namespace
    env.globals['close_namespace'] = close_namespace
    env.globals['get_current_namespace'] = get_current_namespace

    return env


JINJA_ENV = make_env()


class OneToOneFileRenderer:
    def __init__(
        self,
        *,
        relative_to: str,
        vfilepath_to_relfilepath: Dict[str, str],
        clang_format_bin: str,
        parse_extra_formats: bool = False,
        generate_serializer: bool = False,
    ) -> None:
        self._relative_to = relative_to
        self._vfilepath_to_relfilepath_map = vfilepath_to_relfilepath
        self._clang_format_bin = clang_format_bin
        self._parse_extra_formats = parse_extra_formats
        self._generate_serializer = generate_serializer

    @staticmethod
    def filepath_wo_ext(filepath: str) -> str:
        path = pathlib.Path(filepath)
        return str(path.parent / path.stem)

    def _vfilepath_to_relfilepath(self, vfilepath: str) -> str:
        return self._vfilepath_to_relfilepath_map[vfilepath]

    def extract_external_includes(
        self,
        types_cpp: Dict[str, cpp_types.CppType],
        ignore_filepath_wo_ext: str,
    ) -> List[str]:
        result = set()

        def visitor(
            schema: types.Schema, _parent: Optional[types.Schema],
        ) -> None:
            if not isinstance(schema, types.Ref):
                return

            filepath = self.filepath_wo_ext(
                self._vfilepath_to_relfilepath(
                    schema.schema.source_location().filepath,
                ),
            )
            if filepath != ignore_filepath_wo_ext:
                result.add(self.filepath_to_include(filepath))

        for type_ in types_cpp.values():
            assert type_.json_schema
            visitor(type_.json_schema, None)
            type_.json_schema.visit_children(visitor)

        return sorted(result)

    def filepath_to_include(self, filepath_wo_ext: str) -> str:
        if filepath_wo_ext.startswith('/'):
            return os.path.relpath(filepath_wo_ext, self._relative_to) + '.hpp'
        else:
            return filepath_wo_ext + '.hpp'

    def render(
        self,
        types: Dict[str, cpp_types.CppType],
        local_pair_header=True,
        pair_header: Optional[str] = None,
    ) -> List[CppOutput]:
        files: Dict[str, Dict[str, cpp_types.CppType]] = (
            collections.defaultdict(dict)
        )

        for name, type_ in types.items():
            assert type_.json_schema
            filepath = self.filepath_wo_ext(
                self._vfilepath_to_relfilepath(
                    type_.json_schema.source_location().filepath,
                ),
            )
            files[str(filepath)][name] = type_

        if self._parse_extra_formats:
            parse_formats = [
                '::formats::json',
                '::formats::yaml',
                '::yaml_config',
            ]
        else:
            parse_formats = ['::formats::json']

        output = []
        for filepath_wo_ext, types_cpp in files.items():
            external_includes = self.extract_external_includes(
                types_cpp, filepath_wo_ext,
            )

            if pair_header:
                p_header = pair_header
            else:
                if local_pair_header:
                    p_header = os.path.basename(filepath_wo_ext)
                else:
                    p_header = filepath_wo_ext

            env = {
                'pair_header': p_header,
                'types': types_cpp,
                'userver': 'USERVER_NAMESPACE',
                'external_includes': external_includes,
                'parse_formats': parse_formats,
                'generate_serializer': self._generate_serializer,
            }

            tpl = JINJA_ENV.get_template('templates/type_fwd.hpp.jinja')
            fwd_hpp = tpl.render(types=types_cpp)
            fwd_hpp = cpp_format.format_pp(
                fwd_hpp, binary=self._clang_format_bin,
            )

            tpl = JINJA_ENV.get_template('templates/type.hpp.jinja')
            hpp = tpl.render(**env)
            hpp = cpp_format.format_pp(hpp, binary=self._clang_format_bin)

            tpl = JINJA_ENV.get_template('templates/type_parsers.ipp.jinja')
            parsers_ipp = tpl.render(**env)
            parsers_ipp = cpp_format.format_pp(
                parsers_ipp, binary=self._clang_format_bin,
            )

            tpl = JINJA_ENV.get_template('templates/type.cpp.jinja')
            cpp = tpl.render(**env)
            cpp = cpp_format.format_pp(cpp, binary=self._clang_format_bin)

            output.append(
                CppOutput(
                    filepath_wo_ext=filepath_wo_ext,
                    files=[
                        CppOutputFile(
                            content=fwd_hpp, ext='_fwd.hpp', subdir='include/',
                        ),
                        CppOutputFile(
                            content=hpp, ext='.hpp', subdir='include/',
                        ),
                        CppOutputFile(
                            content=parsers_ipp,
                            ext='_parsers.ipp',
                            subdir='include/',
                        ),
                        CppOutputFile(content=cpp, ext='.cpp', subdir='src/'),
                    ],
                ),
            )

        return output
