import argparse
import sys
import re
from datetime import datetime

TL_BASIC_TYPES = [ 'double', 'int', 'int32', 'int53', 'int64', 'string', 'Bool', 'bytes' ]
LINE_BREAK = '\n'

SIGNAL_DEF = "signals:\n    void dataChanged();" 
INCLUDE_TDREQ = "#include \"common/qtdrequest.h\""
INCLUDE_TDCLIENT = "#include \"client/qtdclient.h\""

def current_datetime():
    return datetime.now().date()

def generate_copyright():
    return f"""/*
 * Copyright (C) 2024 UBPorts Foundation
 *
 * WARNING: This is a generated file. Do not modify directly.
 * Any changes made to this file will be overwritten.
 * 
 * Generated on: {current_datetime()}
 * Generated by: TELEports tlparser.py
 * 
 * For any modifications, please edit the source templates and regenerate this file.
 */

"""

def capfirst(s):
    return s[:1].upper() + s[1:]

def lowfirst(s):
    return s[:1].lower() + s[1:]

def squeeze_empty_lines(s):
    a = re.sub(r'\n\s*\n', '\n\n', s)
    return a

def camel_to_underscores(camel_case_str):
    # Use a regular expression to insert an underscore before each uppercase letter
    # that follows a lowercase letter or another uppercase letter (excluding the first character)
    return re.sub(r'(?<!^)(?=[A-Z])', '_', camel_case_str)

def camel_to_upper_with_underscores(camel_case_str):
    # Convert to uppercase
    result = camel_to_underscores(camel_case_str).upper()
    return result

def camel_to_lower_with_underscores(camel_case_str):
    # Convert to uppercase
    result = camel_to_underscores(camel_case_str).lower()
    return result

def parse_tl(tl):
    generate_functions = False
    definitions = []

    lines = tl.strip().split('\n')
    for line in lines:
        line = line.strip()
        if not line:
            continue
        if line.startswith('---functions---'):
            generate_functions = True
            continue
        if line.startswith('vector') | line.startswith('double') | line.startswith('string') | line.startswith('int') | line.startswith('bool') | line.startswith('bytes'):
            continue
        if not line.startswith('//'):  # Ignore comments
            definition = parse_definition(line, generate_functions)
            assert definition != None
            definitions.append(definition)

    return definitions

def parse_definition(line, generate_functions):
    match = re.match(r'(\w+)\s*(.*)\s*=\s*(\w+);', line)
    if not match:
        return None
    name = match.group(1)
    fields = match.group(2).split()
    class_name = match.group(3)
    return {'name': name, 'fields': fields, 'class_name': class_name, 'function': generate_functions}

def generate_field_description(field_definition):
    name, type_def = field_definition.split(':')
    is_vector = False
    if type_def.startswith('vector'):
        is_vector = True
        match = re.search(r'<(.*?)>', type_def)
        type_def =  match.group(1)
    return {'name': name, 'type_def': type_def, 'is_vector': is_vector }

def generate_cpp_field(cpp_type, cpp_field_name, is_basic_type):
    if not is_basic_type:
        return f'    QScopedPointer<{cpp_type}> m_{cpp_field_name};'
    return f'    {cpp_type} m_{cpp_field_name};'

def generate_class_factory(class_name_with_prefix, childtypes):
    if not childtypes:
        return "", ""
    factory_header_str = f"""struct {class_name_with_prefix}Factory
{{
    static {class_name_with_prefix} *create(const QJsonObject &data, QObject *parent);
}};"""
    factory_cpp_str = f"""{class_name_with_prefix} *{class_name_with_prefix}Factory::create(const QJsonObject &data, QObject *parent)
{{
    {class_name_with_prefix} *result = Q_NULLPTR;
    const QString type = data["@type"].toString();
"""
    childtype_selectors = []
    for childtype in childtypes:
        childtype_selectors.append(f"""if (type == \"{lowfirst(childtype[3:])}\") {{
        result = new {childtype}(parent);
    }}""")
    factory_cpp_str = factory_cpp_str + '    ' + ' else '.join(childtype_selectors)
    factory_cpp_str = factory_cpp_str + f"""\n
    if (result) {{
        result->unmarshalJson(data);
    }} else {{
        qWarning() << "{class_name_with_prefix}Factory: Unknown factory type" << type << "received!";
    }}
    return result;
}}"""

    return factory_header_str, factory_cpp_str

def generate_getter_setter_function(class_name_with_prefix, cpp_field_name, cpp_type, is_basic_type):
    result = f'{cpp_type} {class_name_with_prefix}::{cpp_field_name}() const {{'
    if is_basic_type:
        result = result + f'\n    return m_{cpp_field_name};\n}}\n'
        result = result + f'\nvoid {class_name_with_prefix}::set{capfirst(cpp_field_name)}({cpp_type} value) {{\n    if (m_{cpp_field_name} != value) {{\n        m_{cpp_field_name} = value;\n        emit dataChanged();\n    }}\n}}\n'
    else:
        result = result + f'\n    return m_{cpp_field_name}.data();\n}}\n'
        result = result + f'\nvoid {class_name_with_prefix}::set{capfirst(cpp_field_name)}({cpp_type} *value) {{\n    if (m_{cpp_field_name}.data() != value) {{\n        m_{cpp_field_name}.reset(value);\n        emit dataChanged();\n    }}\n}}\n'
    return result

def generate_code(definitions):

    cpp_class_h = ""
    cpp_class_cpp = ""
    cpp_class_h_inner = ""
    cpp_class_cpp_inner = ""
    class_names = []
    root_definition = []
    foreign_class_includes = []

    for definition in definitions:
        name = convert_to_camel_case(definition['name'])
        class_name_with_prefix = generate_classname(name, definition['function'])
        if not root_definition:
            root_definition = definition
            generate_function = root_definition['function']
            root_class_name = generate_classname(name, generate_function)
        else:
            class_names.append(class_name_with_prefix)
        ancestor_class_name = generate_ancestor_class_name(name, definition['class_name'], generate_function)
        cpp_fields = []
        init_params = ['QObject *parent']
        init_body = [f'{ancestor_class_name}(parent)']
        cpp_params = []
        property_declarations = []
        getter_setter_functions = []
        getter_setter_headers = []
        marshal_json_declarations = []
        unmarshal_json_declarations = []

        for field in definition['fields']:
            field_description = generate_field_description(field)
            cpp_field_name = convert_fieldname(field_description['name'])
            inner_cpp_type, is_basic_type = tl_type_to_cpp_type(field_description['type_def'])
            if field_description['is_vector']:
                cpp_type = f'QList<{inner_cpp_type}>'
            else:
                cpp_type = inner_cpp_type
            marshal_json_declarations.append(generate_marshal_json_declaration(cpp_field_name, field_description))
            json_unmarshal_type = tl_type_to_json_unmarshal_type(cpp_field_name, cpp_type, field_description)
            unmarshal_json_declarations.append(json_unmarshal_type)
            cpp_field = generate_cpp_field(cpp_type, cpp_field_name, is_basic_type)
            cpp_fields.append(cpp_field)
            if not is_basic_type:
                foreign_class_includes.append(f'#include \"{inner_cpp_type.lower()}.h\"')
            init_params.append(f'{cpp_type} {cpp_field_name}_')
            init_body.append(f'    m_{cpp_field_name}({cpp_field_name}_)')
            cpp_params.append(f'{cpp_type} {cpp_field_name}')
            property_declarations.append(f'    Q_PROPERTY({cpp_type} {cpp_field_name} READ {cpp_field_name} WRITE set{capfirst(cpp_field_name)} NOTIFY dataChanged)')
            getter_setter_functions.append(generate_getter_setter_function(class_name_with_prefix, cpp_field_name, cpp_type, is_basic_type))
            getter_setter_headers.append(f'    {cpp_type} {cpp_field_name}() const;\n    void set{capfirst(cpp_field_name)}({cpp_type} value);')
        cpp_fields_str = LINE_BREAK.join(cpp_fields)
        init_params_str = ', '.join(init_params)
        init_body_str = ',\n'.join(init_body)
        property_declarations_str = LINE_BREAK.join(property_declarations)
        getter_setter_functions_str = LINE_BREAK.join(getter_setter_functions)
        getter_setter_headers_str = LINE_BREAK.join(getter_setter_headers)
        marshal_json_declarations_str = ',\n'.join(marshal_json_declarations)
        unmarshal_json_declarations_str = LINE_BREAK.join(unmarshal_json_declarations)
        foreign_class_includes_str = LINE_BREAK.join(foreign_class_includes)

        h_initconstructorline = f'explicit {class_name_with_prefix}({init_params_str});'
        cpp_initconstructorline = f'{class_name_with_prefix}::{class_name_with_prefix}({init_params_str}){chr(10)}    : {init_body_str} {{{chr(10)}}}'

        cpp_class_h_inner = cpp_class_h_inner + f"""
/**
 * @brief The {class_name_with_prefix} class
 *
"""
        if name == definition['class_name']:
            cpp_class_h_inner = cpp_class_h_inner + f"""
 * https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1_{camel_to_lower_with_underscores(class_name_with_prefix[3:])}.html
 */
"""
        else:
            cpp_class_h_inner = cpp_class_h_inner + f"""
* https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1{camel_to_lower_with_underscores(class_name_with_prefix[3:])}.html
 */
"""            
        cpp_class_h_inner = cpp_class_h_inner + f"""class {class_name_with_prefix} : public {ancestor_class_name} {{
    {"Q_OBJECT" if not generate_function else "Q_GADGET"}
{property_declarations_str if not generate_function else ""}

public:
    explicit {class_name_with_prefix}(QObject *parent = nullptr);
    {h_initconstructorline if len(init_params) > 1 else ""}

{getter_setter_headers_str if not generate_function else ""}

    {"void unmarshalJson(const QJsonObject &json) override;" if not generate_function else ""}
    QJsonObject marshalJson() override;

private:
    Q_DISABLE_COPY({class_name_with_prefix})
{cpp_fields_str}

{SIGNAL_DEF if not generate_function else ""}

}};
"""
        cpp_class_cpp_inner = cpp_class_cpp_inner + f"""{class_name_with_prefix}::{class_name_with_prefix}(QObject *parent)
    : {ancestor_class_name}(parent)
{{
    setType({camel_to_upper_with_underscores(class_name_with_prefix[3:])});
}}

{cpp_initconstructorline if len(init_params) > 1 else ""}

{getter_setter_functions_str if not generate_function else ""}
QJsonObject {class_name_with_prefix}::marshalJson() {{
    return QJsonObject{{
        {{ "@type", QTdObject::typeString() }},
{marshal_json_declarations_str}
    }};
}}

"""
        if not generate_function:
            cpp_class_cpp_inner = cpp_class_cpp_inner + f"""void {class_name_with_prefix}::unmarshalJson(const QJsonObject &json) {{
    QJsonObject jsonObj;
    QJsonArray jsonArray;
                            
    {ancestor_class_name}::unmarshalJson(json);
{unmarshal_json_declarations_str}
                                    
    emit dataChanged();
}}\n
"""
    class_factory_header_str, class_factory_cpp_str = generate_class_factory(root_class_name, class_names)

    cpp_class_cpp = f"""{generate_copyright()}
    
#include "{root_class_name.lower()}.h"

#include <QJsonObject>
#include <QJsonArray>
#include <QDebug>

""" + cpp_class_cpp_inner + f"""
{class_factory_cpp_str}
"""

    cpp_class_h = f"""{generate_copyright()}
#ifndef {root_class_name.upper()}_H
#define {root_class_name.upper()}_H

#include <QObject>
#include <QList>

#include "common/qabstracttdobject.h"
{INCLUDE_TDREQ if generate_function else ""}
{INCLUDE_TDCLIENT if generate_function else ""}
{foreign_class_includes_str}

    """ + cpp_class_h_inner + f"""
{class_factory_header_str}

#endif // {root_class_name.upper()}_H
"""
    return squeeze_empty_lines(cpp_class_h), squeeze_empty_lines(cpp_class_cpp), root_class_name

def generate_marshal_json_declaration(cpp_field_name, field_description):
    if field_description['is_vector']:
        return f"""        {{ "{field_description['name']}", QTdObject::marshalArrayValue(m_{cpp_field_name}) }}"""
    if field_description['type_def'] in TL_BASIC_TYPES:
        return f"""        {{ "{field_description['name']}", m_{cpp_field_name} }}"""
    return f"""        {{ "{field_description['name']}", m_{cpp_field_name}->marshalJson() }}"""


def tl_type_to_json_unmarshal_type(cpp_field_name, cpp_type, field_description):

    unmarshal_cpp_type = tl_type_to_unmarshal_type(field_description['type_def'])
    if field_description['is_vector']:
        if field_description['type_def'] in TL_BASIC_TYPES:
            return f"""    for (const QJsonValue &jsonValue : json["{field_description['name']}"].toArray()) {{\n        m_{cpp_field_name} << jsonValue.{unmarshal_cpp_type};\n    }}"""
        return f"""    for (const QJsonValue &jsonValue : json["{field_description['name']}"].toArray()) {{\n        m_{cpp_field_name} << ;\n    }}"""

    if field_description['type_def'] in TL_BASIC_TYPES:
        return f"""    m_{cpp_field_name} = json["{field_description['name']}"].{unmarshal_cpp_type};"""
    return f"""    m_{cpp_field_name}.reset(new {cpp_type[:-1]}(this));\n    m_{cpp_field_name}->unmarshalJson(json["{field_description['name']}"].toObject());"""
        
def check_for_ancestor(definition):
    if definition['name'].lower() == definition['class_name'].lower():
        return
    if definition['function']:
        return
    return {'name': definition['class_name'], 'fields': '', 'class_name': definition['class_name'], 'function': False}

def generate_classname(classname, generate_function):
    if generate_function:
        return f"QTd{classname}Request"
    return f"QTd{classname}"

def generate_ancestor_class_name(tl_class_name, tl_ancestor_name, generate_function):
    if generate_function:
        if tl_ancestor_name == 'Ok':
            return 'QTdOkRequest'
        else:
            return 'QTdRequest'
    if tl_class_name != tl_ancestor_name:
        return f'QTd{tl_ancestor_name}'
    return 'QTdObject'

def tl_type_to_unmarshal_type(tl_type):
    return {
        'int': 'toInt()',
        'int32': 'toInt()',
        'int53': 'toVariant().toLongLong()',  # Since C++ doesn't have int53, we map it to int64
        'int64': 'toVariant().toLongLong()',
        'double': 'toDouble()',
        'string': 'toString()',
        'bytes': 'toVariant().toByteArray()',
        'Bool': 'toBool()'
    }.get(tl_type, 'ERROR-unknown-type')

def tl_type_to_cpp_type(tl_type):
    is_basic_type = True
    cpp_type = {
        'int': 'qint16',
        'int32': 'qint32',
        'int53': 'qint64',  # Since C++ doesn't have int53, we map it to int64
        'int64': 'qint64',
        'double': 'double',
        'string': 'QString',
        'bytes': 'QByteArray',
        'Bool': 'bool',
    }.get(tl_type, f'QTd{convert_to_camel_case(tl_type)}')
    if (cpp_type.startswith('QTd')):
        is_basic_type = False
    return cpp_type, is_basic_type

def convert_fieldname(name):
    components = name.split('_')
    result = ''.join(x.capitalize() for x in components)
    return lowfirst(result)

def convert_to_camel_case(name):
    components = name.split('_')
    if len(components) == 1:
        return capfirst(name)
    result = ''.join(x.capitalize() for x in components)
    return lowfirst(result)

def generate_cpp_code(tl):
    definitions = parse_tl(tl)
    cpp_headers = []
    cpp_implementations = []

    class_map = {}

    for definition in definitions:
        ancestor_class = check_for_ancestor(definition)
        if ancestor_class:
            if ancestor_class['name'] not in class_map:
                class_map[ancestor_class['name']] = [ ancestor_class ]
            class_map[ancestor_class['name']].append(definition)
        else:
            class_map[definition['name']] = [ definition ]

    for _, value_list in class_map.items():
        cpp_header, cpp_implementation, class_name = generate_code(value_list)
        cpp_headers.append((class_name, cpp_header))
        cpp_implementations.append((class_name, cpp_implementation))

    return cpp_headers, cpp_implementations

def save_to_files(cpp_headers, cpp_implementations):
    for class_name, cpp_content in cpp_headers:
        header_filename = f'{class_name.lower()}.h'
        with open(header_filename, 'w') as f:
            f.write(cpp_content)
        print(f'Saved {header_filename}')

    for class_name, cpp_content in cpp_implementations:
        implementation_filename = f'{class_name.lower()}.cpp'
        with open(implementation_filename, 'w') as f:
            f.write(cpp_content)
        print(f'Saved {implementation_filename}')

def main():
    parser = argparse.ArgumentParser(description='Generate tdlib C++ classes from TL definitions')
    parser.add_argument('file', nargs='?', type=argparse.FileType('r'), default=sys.stdin,
                        help='Input file or stdin if not specified')

    args = parser.parse_args()
    tl_description = args.file.read()

    cpp_headers, cpp_implementations = generate_cpp_code(tl_description)
    save_to_files(cpp_headers, cpp_implementations)

if __name__ == "__main__":
    main()