# 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 validate_annotations(annotations):
    """Ensure that the annotations have all the required fields"""

    for (name, data) in sorted(annotations.items()):
        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)
        else:
            annotation_type = data.get("type")
            valid_types = ["boolean", "integer", "string"]
            if not any(annotation_type == t for t in valid_types):
                print(
                    "Annotation "
                    + name
                    + " has an unknown type: "
                    + annotation_type
                    + "\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, "r") as annotations_file:
            annotations = yaml.safe_load(annotations_file)
    except (IOError, 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, "r") as template_file:
            template = template_file.read()
    except IOError as ex:
        print("Error when reading " + template_filename + ":\n" + str(ex) + "\n")
        sys.exit(1)

    return template


def extract_crash_ping_allowlist(annotations):
    """Extract an array holding the names of the annotations allowlisted for
    inclusion in the crash ping."""

    return [
        name for (name, data) in sorted(annotations.items()) if data.get("ping", False)
    ]


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


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

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

    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(sorted(annotations.items())):
        enum += "  " + name + " = " + str(i) + ",\n"

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

    return enum


def generate_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_header(template, annotations):
    """Generate a header by filling the template with the the list of
    annotations and return it as a string."""

    allowlist = extract_crash_ping_allowlist(annotations)

    return template_header + string.Template(template).substitute(
        {
            "enum": generate_enum(annotations),
            "strings": generate_strings(annotations),
            "allowlist": generate_array_initializer(allowlist),
        }
    )


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 IOError 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."""

    allowlist = extract_crash_ping_allowlist(annotations)

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


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_ALLOWLIST = {
    ${allowlist}
        };
    }"""
    )

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

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