import pathlib
from typing import List, Tuple, Optional
from jinja2 import Template
from ..json_schema import JsonSchema
from ..json_schema_parser import JsonSchemaParser

CPP_BEGIN = Template('''// this is generated by sukonbu
#pragma once
#include <optional>
#include <vector>
#include <string>
#include <unordered_map>

namespace {{ namespace }} {
''')

CPP_END = '''} // end of namespace
'''

CPP_ENUM = Template('''
enum class {{ class_name }}
{
{%- for prop in props %}
    {{ prop }},
{%- endfor %}
};

''')

CPP_CLASS = Template('''
struct {{ class_name }}
{
{%- for prop in props %}
    // {{ prop[0] }}
    {{ prop[1] }};
{%- endfor %}
};

''')


def js_to_cpp_type(name: str, js: JsonSchema, parent: JsonSchema) -> str:
    '''
    convert JsonSchema to cpp type
    '''
    if js.title in ['Extension', 'Extras']:
        return js.get_class_name()

    enum_values = js.enumerate_values()
    if enum_values:
        return js.title

    if js.type == 'integer':
        return 'int'
    if js.type == 'number':
        return 'float'
    elif js.type == 'boolean':
        return 'bool'
    elif js.type == 'string':
        return 'std::string'
    elif js.type == 'object':
        if js.properties and js.additionalProperties:
            raise Exception()
        if js.properties:
            return js.get_class_name()
        elif js.additionalProperties:
            return f'std::unordered_map<std::string, {js_to_cpp_type(name, js.additionalProperties, js)}>'
        else:
            return 'Object[string]'
    elif js.type == 'array':
        return f'std::vector<{js_to_cpp_type(name, js.items, js)}>'
    elif js.type == 'unknown':
        return 'Object[string]'
    else:
        raise Exception()
        # return js.type, 'None'


def escape_enum(src: str) -> str:
    splitted = src.split('/')
    if len(splitted) > 1:
        # ex. image/jpg
        return ''.join(x for x in splitted)
    else:
        return src


def enum_value(src: str) -> str:
    splitted = src.split('=')
    if len(splitted) == 2:
        # int
        return f'{escape_enum(splitted[0])} = {splitted[1]}'
    else:
        # str
        return f'{escape_enum(splitted[0])} /* = "{splitted[0]}" */'


def enum_read_func(js: JsonSchema) -> str:
    if js.type == 'integer':
        return f'p = ({js.title})j.get<int>();'
    elif js.type == 'string':
        func = f'auto value = j.get<std::string>();'
        for enum_value in js.enumerate_values():
            func += f'\n    if(value=="{enum_value}")p = {js.title}::{escape_enum(enum_value)};'
        return func
    else:
        raise NotImplementedError()


def add_optional(js: JsonSchema, src: str, required: bool) -> str:
    if js.type == 'string' and js.enumerate_values():
        return f'std::optional<{src}>'

    if js.type in ['string', 'array', 'enum']:
        return f'{src}'
    if js.type == 'object':
        if js.additionalProperties:
            return f'{src}'

    return f'std::optional<{src}>'


def escape_symbol(name: str) -> str:
    return name


def read_func(name: str, js: JsonSchema, parent: Optional[JsonSchema]) -> str:
    '''
    fromJSON
    '''

    # if js.type in ['array', 'string']:
    #     return f'if(j.find("{name}")!=j.end()) j.at("{name}").get_to(p.{escape_symbol(name)});'

    # if js.type == 'object' and js.additionalProperties:
    #     return f'if(j.find("{name}")!=j.end()) j.at("{name}").get_to(p.{escape_symbol(name)});'

    return f'if(j.find("{name}")!=j.end()) j.at("{name}").get_to(p.{escape_symbol(name)});'


def write_func(name: str, js: JsonSchema, parent: Optional[JsonSchema]) -> str:
    '''
    to_dict
    '''
    return ''


NLOHMANN_BEGIN = Template('''// this is generated by sukonbu
#pragma once
#include "{{ header_file }}"
#include <nlohmann/json.hpp>

// partial specialization (full specialization works too)
namespace nlohmann
{
template <typename T>
struct adl_serializer<std::optional<T>>
{
    static void to_json(json &j, const std::optional<T> &opt)
    {
        if (opt.has_value())
        {
            j = *opt;
        }
    }
    static void from_json(const json &j, std::optional<T> &opt)
    {
        if (!j.is_null())
        {
            opt = j.get<T>();                               
        }
    }
};
} // namespace nlohmann

namespace {{ namespace }} {
using nlohmann::json;

''')

NLOHMANN_END = '''} // end of namespace
'''

NLOHMANN_SERIALIZATION = Template('''
void to_json(json& j, const {{ class_name }} & p) {
{%- for write in writes %}
    {{ write }}
{%- endfor %}
}
void from_json(const json& j, {{ class_name }}& p) {
{%- for read in reads %}
    {{ read }}
{%- endfor %}
}

''')


def generate(parser: JsonSchemaParser, dst: pathlib.Path,
             namespace: str) -> None:

    print(f'write: {dst}')
    dst.parent.mkdir(parents=True, exist_ok=True)
    with dst.open('w') as w:
        w.write(CPP_BEGIN.render(namespace=namespace))

        for key, js, parent in parser.schemas:
            enum_values = js.get_enum_values()
            if enum_values:
                if not parent:
                    raise Exception()
                value_map = {
                    'class_name': js.title,
                    'props': [enum_value(value) for value in enum_values],
                }
                w.write(CPP_ENUM.render(**value_map))

            elif js.properties:
                props = [(
                    v.description,
                    f'{add_optional(v, js_to_cpp_type(k, v, js), k in js.required)} {escape_symbol(k)}'
                ) for k, v in js.properties.items()]
                value_map = {
                    'class_name':
                    js.get_class_name(),
                    'props':
                    sorted(props, key=lambda x: '=' in x[1]),
                    'writes':
                    [write_func(k, v, js) for k, v in js.properties.items()],
                    'reads':
                    [read_func(k, v, js) for k, v in js.properties.items()]
                }
                w.write(CPP_CLASS.render(**value_map))

            elif js.title in ['Extension', 'Extras']:
                if not parent:
                    raise Exception()
                value_map = {
                    'class_name': js.get_class_name(),
                    'props': [],
                    'writes': [],
                    'reads': [],
                }
                w.write(CPP_CLASS.render(**value_map))

        w.write(CPP_END)

    nlohmann_json = dst.parent / (dst.stem + '_nlohmann_json.h')
    with nlohmann_json.open('w') as w:
        w.write(
            NLOHMANN_BEGIN.render(namespace=namespace, header_file=dst.name))

        for key, js, parent in parser.schemas:
            enum_values = js.get_enum_values()
            if enum_values:
                if not parent:
                    raise Exception()
                value_map = {
                    'class_name': js.title,
                    'props': [enum_value(value) for value in enum_values],
                    'writes': [],
                    'reads': [enum_read_func(js)]
                }
                w.write(NLOHMANN_SERIALIZATION.render(**value_map))

            elif js.properties:
                props = [(
                    v.description,
                    f'{add_optional(v, js_to_cpp_type(k, v, js), k in js.required)} {escape_symbol(k)}'
                ) for k, v in js.properties.items()]
                value_map = {
                    'class_name':
                    js.get_class_name(),
                    'props':
                    sorted(props, key=lambda x: '=' in x[1]),
                    'writes':
                    [write_func(k, v, js) for k, v in js.properties.items()],
                    'reads':
                    [read_func(k, v, js) for k, v in js.properties.items()]
                }
                w.write(NLOHMANN_SERIALIZATION.render(**value_map))

            elif js.title in ['Extension', 'Extras']:
                if not parent:
                    raise Exception()
                value_map = {
                    'class_name': js.get_class_name(),
                    'props': [],
                    'writes': [],
                    'reads': [],
                }
                w.write(NLOHMANN_SERIALIZATION.render(**value_map))

        w.write(NLOHMANN_END)

    print(f'write: {nlohmann_json}')
