# 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
import textwrap

import yaml

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

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


def sort_annotations(annotations):
    """Return annotations in ascending alphabetical order ignoring case"""

    return sorted(annotations.items(), key=lambda annotation: str.lower(annotation[0]))


def validate_annotations(annotations):
    """Ensure that the annotations have all the required fields"""

    for name, data in annotations:
        if "description" not in data:
            print("Annotation " + name + " does not have a description\n")
            sys.exit(1)
        if "type" not in data:
            print("Annotation " + name + " does not have a type\n")
            sys.exit(1)

        annotation_type = data.get("type")
        valid_types = ["string", "boolean", "u32", "u64", "usize", "object"]
        if annotation_type not in valid_types:
            print(
                "Annotation " + name + " has an unknown type: " + annotation_type + "\n"
            )
            sys.exit(1)

        annotation_scope = data.get("scope", "client")
        valid_scopes = ["client", "report", "ping"]
        if annotation_scope not in valid_scopes:
            print(
                "Annotation "
                + name
                + " has an unknown scope: "
                + annotation_scope
                + "\n"
            )
            sys.exit(1)


def read_annotations(annotations_filename):
    """Read the annotations from a YAML file.
    If an error is encountered quit the program."""

    try:
        with open(annotations_filename) as annotations_file:
            annotations = sort_annotations(yaml.safe_load(annotations_file))
    except (OSError, ValueError) as e:
        print("Error parsing " + annotations_filename + ":\n" + str(e) + "\n")
        sys.exit(1)

    validate_annotations(annotations)

    return annotations


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:
        print("Error when reading " + template_filename + ":\n" + str(ex) + "\n")
        sys.exit(1)

    return template


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

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


def extract_crash_report_allowedlist(annotations):
    """Extract an array holding the names of the annotations allowed for
    inclusion in a crash report (excluding those allowed for pings)."""

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


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]


###############################################################################
# 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)


def generate_header(template, annotations):
    """Generate a header by filling the template with the the list of
    annotations and return it as a string."""

    pingallowedlist = extract_crash_ping_allowedlist(annotations)
    reportallowedlist = extract_crash_report_allowedlist(annotations)
    skiplist = extract_skiplist(annotations)
    typelist = extract_types(annotations)

    return template_header + string.Template(template).substitute(
        {
            "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),
        }
    )


def emit_header(output, template_filename, annotations_filename):
    """Generate the C++ header from the template and write it out."""

    annotations = read_annotations(annotations_filename)
    template = read_template(template_filename)
    generated_header = generate_header(template, annotations)

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


###############################################################################
# Java code generation                                                        #
###############################################################################


def generate_java_array_initializer(contents):
    """Generates the initializer for an array of strings.
    Effectively turns `["a", "b"]` into '   \"a\",\n   \"b\"\n'."""

    initializer = ""

    for name in contents:
        initializer += '        "' + name + '",\n'

    return initializer.strip(",\n")


def generate_class(template, annotations):
    """Fill the class template from the list of annotations."""

    allowedlist = extract_crash_ping_allowedlist(annotations)

    return template_header + string.Template(template).substitute(
        {
            "allowedlist": generate_java_array_initializer(allowedlist),
        }
    )


def emit_class(output, annotations_filename):
    """Generate the CrashReporterConstants.java file."""

    template = textwrap.dedent(
        """\
    package org.mozilla.gecko;

    /**
     * Constants used by the crash reporter. These are generated so that they
     * are kept in sync with the other C++ and JS users.
     */
    public class CrashReporterConstants {
        public static final String[] ANNOTATION_PING_ALLOWEDLIST = {
    ${allowedlist}
        };
    }"""
    )

    annotations = read_annotations(annotations_filename)
    generated_class = generate_class(template, annotations)

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