# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

import string
import sys
from os import path

import yaml

###############################################################################
# Language-agnostic functionality                                             #
###############################################################################

template_header = (
    "/* This file was autogenerated by "
    "toolkit/crashreporter/annotations/generate.py. DO NOT EDIT */\n\n"
)


def read_template(template_filename):
    """Read the contents of the template.
    If an error is encountered quit the program."""

    try:
        with open(template_filename) as template_file:
            template = template_file.read()
    except OSError as ex:
        sys.exit("Error when reading " + template_filename + ":\n" + str(ex) + "\n")

    return template


def extract_crash_scope_list(annotations, scope):
    """Extract an array holding the names of the annotations allowed for
    inclusion in a crash scope."""

    return [
        name for (name, data) in annotations if data.get("scope", "client") == scope
    ]


def extract_skiplist(annotations):
    """Extract an array holding the names of the annotations that should be
    skipped and the values which will cause them to be skipped."""

    return [
        (name, data.get("skip_if"))
        for (name, data) in annotations
        if len(data.get("skip_if", "")) > 0
    ]


def type_to_enum(annotation_type):
    """Emit the enum value corresponding to each annotation type."""

    if annotation_type == "string":
        return "String"
    elif annotation_type == "boolean":
        return "Boolean"
    elif annotation_type == "u32":
        return "U32"
    elif annotation_type == "u64":
        return "U64"
    elif annotation_type == "usize":
        return "USize"
    elif annotation_type == "object":
        return "Object"


def extract_types(annotations):
    """Extract an array holding the type of each annotation."""

    return [type_to_enum(data.get("type")) for (_, data) in annotations]


generators = {}


def content_generator(*extensions):
    """Create a function that generates the content for a file extension."""

    def f(func):
        for e in extensions:
            generators[e] = func

    return f


###############################################################################
# C++ code generation                                                         #
###############################################################################


def generate_strings(annotations):
    """Generate strings corresponding to every annotation."""

    names = ['  "' + data.get("altname", name) + '"' for (name, data) in annotations]

    return ",\n".join(names)


def generate_enum(annotations):
    """Generate the C++ typed enum holding all the annotations and return it
    as a string."""

    enum = ""

    for i, (name, _) in enumerate(annotations):
        enum += "  " + name + " = " + str(i) + ",\n"

    enum += "  Count = " + str(len(annotations))

    return enum


def generate_annotations_array_initializer(contents):
    """Generates the initializer for a C++ array of annotations."""

    initializer = ["  Annotation::" + name for name in contents]

    return ",\n".join(initializer)


def generate_skiplist_initializer(contents):
    """Generates the initializer for a C++ array of AnnotationSkipValue structs."""

    initializer = [
        "  { Annotation::" + name + ', "' + value + '" }' for (name, value) in contents
    ]

    return ",\n".join(initializer)


def generate_types_initializer(contents):
    """Generates the initializer for a C++ array of AnnotationType values."""

    initializer = ["  AnnotationType::" + typename for typename in contents]

    return ",\n".join(initializer)


@content_generator("h")
def emit_header(annotations, _output_name):
    pingallowedlist = extract_crash_scope_list(annotations, "ping")
    reportallowedlist = extract_crash_scope_list(annotations, "report")
    skiplist = extract_skiplist(annotations)
    typelist = extract_types(annotations)

    return {
        "enum": generate_enum(annotations),
        "strings": generate_strings(annotations),
        "pingallowedlist": generate_annotations_array_initializer(pingallowedlist),
        "reportallowedlist": generate_annotations_array_initializer(reportallowedlist),
        "skiplist": generate_skiplist_initializer(skiplist),
        "types": generate_types_initializer(typelist),
    }


###############################################################################
# Java/Kotlin code generation                                                 #
###############################################################################


def javadoc_sanitize(s):
    return (
        s
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace("@", "&#064;")
        # Kotlin supports nested comments, so change anything that looks like the start of a block comment.
        .replace("/*", "/&#042;")
    )


def derive_package_and_class(file_path):
    """
    Determine the appropriate package and class name for a file path, and
    return whether a kotlin source should be generated rather than java.
    """
    path = file_path.split("src/main/java/", 1)[1]
    package_path, klass_path = path.rsplit("/", 1)
    package = package_path.replace("/", ".")
    is_kotlin = klass_path.endswith(".kt")
    klass = klass_path.removesuffix(".kt").removesuffix(".java")
    return package, klass, is_kotlin


@content_generator("java", "kt")
def emit_java(annotations, output_name):
    package, klass, is_kotlin = derive_package_and_class(output_name)

    enum = ",\n".join(
        f'/** {javadoc_sanitize(data["description"])} */\n{name}("{name}", "{data.get("scope", "client")}")'
        for (name, data) in annotations
    )

    return {
        "package": package,
        "enum": enum,
        "class": klass,
    }


# Main script entrypoint for GeneratedFile
def main(output, annotations_path):
    with open(annotations_path) as annotations_file:
        annotations = yaml.safe_load(annotations_file)

    suffix = output.name.rpartition(".")[2]
    generator = generators.get(suffix)
    if generator is None:
        sys.exit(f"No generator for .{suffix} files")

    template_file = f"CrashAnnotations.{suffix}.in"
    template_path = path.join(path.dirname(__file__), template_file)
    template_args = generator(annotations, output.name)

    content = template_header + string.Template(
        read_template(template_path)
    ).substitute(template_args)

    try:
        output.write(content)
    except OSError as ex:
        sys.exit("Error while writing out the generated file:\n" + str(ex) + "\n")

    return {template_path}
