# -*- coding: utf-8 -*-

# 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/.

"""
Outputter to generate Rust code for metrics.
"""

import enum
import json
from pathlib import Path
from typing import Any, Dict, Optional, Union

from . import __version__
from . import metrics
from . import pings
from . import tags
from . import util


def rust_datatypes_filter(value):
    """
    A Jinja2 filter that renders Rust literals.

    Based on Python's JSONEncoder, but overrides:
      - dicts and sets to raise an error
      - sets to vec![] (used in labels)
      - enums to become Class::Value
      - lists to vec![] (used in send_in_pings)
      - null to None
      - strings to "value".into()
      - Rate objects to a CommonMetricData initializer
        (for external Denominators' Numerators lists)
    """

    class RustEncoder(json.JSONEncoder):
        def iterencode(self, value):
            if isinstance(value, dict):
                raise ValueError("RustEncoder doesn't know dicts {}".format(str(value)))
            elif isinstance(value, enum.Enum):
                yield (value.__class__.__name__ + "::" + util.Camelize(value.name))
            elif isinstance(value, set):
                yield "vec!["
                first = True
                for subvalue in sorted(list(value)):
                    if not first:
                        yield ", "
                    yield from self.iterencode(subvalue)
                    first = False
                yield "]"
            elif isinstance(value, list):
                yield "vec!["
                first = True
                for subvalue in list(value):
                    if not first:
                        yield ", "
                    yield from self.iterencode(subvalue)
                    first = False
                yield "]"
            elif value is None:
                yield "None"
            # `CowStr` is a `str`, so needs to be before next case
            elif isinstance(value, metrics.CowString):
                yield f'::std::borrow::Cow::from("{value.inner}")'
            elif isinstance(value, str):
                yield f"{json.dumps(value)}.into()"
            elif isinstance(value, metrics.Rate):
                yield "CommonMetricData("
                first = True
                for arg_name in util.common_metric_args:
                    if hasattr(value, arg_name):
                        if not first:
                            yield ", "
                        yield f"{util.camelize(arg_name)} = "
                        yield from self.iterencode(getattr(value, arg_name))
                        first = False
                yield ")"
            else:
                yield from super().iterencode(value)

    return "".join(RustEncoder().iterencode(value))


def ctor(obj):
    """
    Returns the scope and name of the constructor to use for a metric object.
    Necessary because LabeledMetric<T> is constructed using LabeledMetric::new
    not LabeledMetric<T>::new
    """
    if getattr(obj, "labeled", False):
        return "LabeledMetric::new"
    return class_name(obj.type) + "::new"


def type_name(obj):
    """
    Returns the Rust type to use for a given metric or ping object.
    """

    if getattr(obj, "labeled", False):
        return "LabeledMetric<{}>".format(class_name(obj.type))
    generate_enums = getattr(obj, "_generate_enums", [])  # Extra Keys? Reasons?
    if len(generate_enums):
        generic = None
        for name, suffix in generate_enums:
            if len(getattr(obj, name)):
                generic = util.Camelize(obj.name) + suffix
            else:
                if isinstance(obj, metrics.Event):
                    generic = "NoExtraKeys"
                else:
                    generic = "No" + suffix

        return "{}<{}>".format(class_name(obj.type), generic)

    generate_structure = getattr(obj, "_generate_structure", [])
    if len(generate_structure):
        generic = util.Camelize(obj.name) + "Object"
        return "{}<{}>".format(class_name(obj.type), generic)

    return class_name(obj.type)


def extra_type_name(typ: str) -> str:
    """
    Returns the corresponding Rust type for event's extra key types.
    """

    if typ == "boolean":
        return "bool"
    elif typ == "string":
        return "String"
    elif typ == "quantity":
        return "u32"
    else:
        return "UNSUPPORTED"


def structure_type_name(typ: str) -> str:
    """
    Returns the corresponding Rust type for structure items.
    """

    if typ == "boolean":
        return "bool"
    elif typ == "string":
        return "String"
    elif typ == "number":
        return "i64"
    else:
        return "UNSUPPORTED"


def class_name(obj_type):
    """
    Returns the Rust class name for a given metric or ping type.
    """
    if obj_type == "ping":
        return "Ping"
    if obj_type.startswith("labeled_"):
        obj_type = obj_type[8:]
    return util.Camelize(obj_type) + "Metric"


def extra_keys(allowed_extra_keys):
    """
    Returns the &'static [&'static str] ALLOWED_EXTRA_KEYS for impl ExtraKeys
    """
    return "&[" + ", ".join([f'"{key}"' for key in allowed_extra_keys]) + "]"


class Category:
    """
    Data struct holding information about a metric to be used in the template.
    """

    def __init__(
        self,
        name: str,
        objs: Dict[str, Union[metrics.Metric, pings.Ping, tags.Tag]],
        contains_pings: bool,
    ):
        self.name = name
        self.objs = objs
        self.contains_pings = contains_pings


def output_rust(
    objs: metrics.ObjectTree, output_dir: Path, options: Optional[Dict[str, Any]] = None
) -> None:
    """
    Given a tree of objects, output Rust code to `output_dir`.

    :param objs: A tree of objects (metrics and pings) as returned from
        `parser.parse_objects`.
    :param output_dir: Path to an output directory to write to.
    :param options: options dictionary, not currently used for Rust
    """

    if options is None:
        options = {}

    template = util.get_jinja2_template(
        "rust.jinja2",
        filters=(
            ("rust", rust_datatypes_filter),
            ("snake_case", util.snake_case),
            ("camelize", util.camelize),
            ("type_name", type_name),
            ("extra_type_name", extra_type_name),
            ("structure_type_name", structure_type_name),
            ("ctor", ctor),
            ("extra_keys", extra_keys),
        ),
    )

    filename = "glean_metrics.rs"
    filepath = output_dir / filename
    categories = []

    for category_key, category_val in objs.items():
        contains_pings = any(
            isinstance(obj, pings.Ping) for obj in category_val.values()
        )

        cat = Category(category_key, category_val, contains_pings)
        categories.append(cat)

    with filepath.open("w", encoding="utf-8") as fd:
        fd.write(
            template.render(
                parser_version=__version__,
                categories=categories,
                extra_metric_args=util.extra_metric_args,
                common_metric_args=util.common_metric_args,
            )
        )
