from posixpath import dirname
from textwrap import indent
from typing import *
from susc import SusFile
from susc.things import *
from os import makedirs, path
from susc import log
from colorama import Fore

LICENSE = """ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the “Software”), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
 * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."""

def snake_to_pascal(name: str) -> str:
    words = name.split("_")
    return "".join([w[0].upper() + w[1:] for w in words])
def snake_to_camel(name: str) -> str:
    pascal = snake_to_pascal(name)
    return pascal[0].lower() + pascal[1:]

def regex_flags(pattern: re.Pattern):
    fstr = ""
    if pattern.flags & re.I: fstr += "i"
    if pattern.flags & re.M: fstr += "m"
    if pattern.flags & re.S: fstr += "s"
    return fstr
def type_validators(type_: SusType):
    vals = []
    for validator in type_.validators:
        val = validator.param + ": "
        restr = validator.restriction
        if isinstance(restr, re.Pattern): val += f"/{restr.pattern}/{regex_flags(restr)}"
        elif isinstance(restr, int): val += str(restr)
        elif isinstance(restr, str): val += restr
        elif isinstance(restr, range): val += f"[{restr[0]}, {restr[-1]}]"
        vals.append(val)
    if len(vals) == 0:
        return "{}"
    return "{ " + ', '.join(vals) + " }"
def type_to_amogus(type_: SusType, obj_types: Dict[str, str]) -> str:
    if type_.name == "List":
        return f"new amogus.repr.List({type_to_amogus(type_.args[0])}, {type_.args[1]}, {type_validators(type_)})"

    if type_.name in obj_types:
        t_name = {
            "entities": "SpecificEntity",
            "bitfields": "Bitfield",
            "enums": "Enum",
            "confirmations": "Confirmation"
        }[obj_types[type_.name]]
        return f"new amogus.repr.{t_name}({obj_types[type_.name]}.{type_.name})"

    return f"new amogus.repr.{type_.name}({', '.join(type_.args + [''])}{type_validators(type_)})"

def write_output(root_file: SusFile, target_dir: str) -> None:
    proj_name = path.splitext(path.basename(root_file.path))[0]
    header = ("/* Generated by AMOGUS SUS (https://github.com/portasynthinca3/amogus)\n"
    f" * Project name: {proj_name}\n *\n"
    "" + LICENSE + "\n */\n\n")

    # display lib notice
    lib_path = path.abspath(path.join(dirname(__file__), "amogus"))
    log.info(f"TypeScript: Install the {Fore.GREEN}'amogus-driver'{Fore.WHITE} library from npm to make use of this output")

    # construct a name-to-type mapping
    objs = {}
    for thing in root_file.things:
        name = type(thing).__name__[3:].lower() + "s"
        if name == "entitys": # correct plural form
            name = "entities"
        objs[thing.name] = name

    # write methods
    log.verbose("Writing methods.ts")
    methods = [t for t in root_file.things if isinstance(t, SusMethod)]
    with open(path.join(target_dir, "methods.ts"), "w") as f:
        f.write(header)
        f.write("import * as amogus from \"amogus\";\n")
        f.write("import * as enums from \"./enums\";\n")
        f.write("import * as bitfields from \"./bitfields\";\n")
        f.write("import * as confirmations from \"./confirmations\";\n")
        f.write("import * as entities from \"./entities\";\n\n")
        for method in methods:
            # write spec
            name = snake_to_pascal(method.name)
            f.write(f"const {name}Spec = {'{'}\n")
            f.write("\tparams: {\n")
            write_field_array(f, method.parameters, objs)
            f.write("\t},\n")
            f.write("\treturns: {\n")
            write_field_array(f, method.returns, objs)
            f.write("\t},\n")
            confirmations = ", ".join("confirmations." + snake_to_pascal(conf) for conf in method.confirmations)
            f.write(f"\tconfirmations: [{confirmations}]\n")
            f.write("};\n")
            # write class
            f.write(f"export class {name} extends amogus.Method<typeof {name}Spec> {'{'}\n")
            f.write("\tconstructor() {\n")
            f.write(f"\t\tsuper({name}Spec, {method.value}, undefined);\n")
            f.write("\t}\n")
            f.write("}\n")
            # write function
            f.write(f"export async function {snake_to_camel(method.name)}(\n")
            f.write("\tsession: amogus.session.Session,\n")
            f.write(f"\tparams: amogus.FieldValue<typeof {name}Spec[\"params\"]>,\n")
            f.write(f"\tconfirm?: amogus.session.ConfCallback<{name}>\n")
            f.write(f"): Promise<amogus.FieldValue<typeof {name}Spec[\"returns\"]>> {'{'}\n")
            f.write(f"\tconst method = new {name}();\n")
            f.write(f"\tmethod.params = params;\n")
            f.write(f"\treturn await session.invokeMethod(method, confirm);\n")
            f.write("}\n\n")

    # write index
    log.verbose("Writing index.ts")
    with open(path.join(target_dir, "index.ts"), "w") as f:
        f.write(header)
        f.write("import * as amogus from \"amogus\";\n")
        f.write("import * as enums from \"./enums\";\n")
        f.write("import * as bitfields from \"./bitfields\";\n")
        f.write("import * as confirmations from \"./confirmations\";\n")
        f.write("import * as entities from \"./entities\";\n")
        f.write("import * as methods from \"./methods\";\n")
        f.write("export { enums, bitfields, confirmations, entities, methods };\n\n")
        # write spec space
        f.write("export const specSpace = {\n")
        f.write("\tglobalMethods: {\n")
        for method in methods:
            f.write(f"\t\t{method.value}: methods.{snake_to_pascal(method.name)},\n")
        f.write("\t}\n")
        f.write("};\n")

def write_field_array(f, fields, objs, indent=2):
    indent = "\t" * indent
    f.write(f"{indent}required: {'{'}\n")
    for param in [f for f in fields if f.optional is None]:
        f.write(f"{indent}\t{param.name}: {type_to_amogus(param.type_, objs)},\n")
    f.write(f"{indent}{'}'},\n")
    f.write(f"{indent}optional: {'{'}\n")
    for param in [f for f in fields if f.optional is not None]:
        f.write(f"{indent}\t{param.name}: {type_to_amogus(param.type_, objs)},\n")
    f.write(f"{indent}{'}'},\n")
