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


def read_conf(conf_filename):
    # Can't read/write from a single StringIO, so make a new one for reading.
    stream = open(conf_filename)

    def parse_counters(stream):
        for line_num, full_line in enumerate(stream):
            line = full_line.rstrip("\n")
            if not line or line.startswith("//"):
                # empty line or comment
                continue
            m = re.match(r"method ([A-Za-z0-9]+)\.([A-Za-z0-9]+)$", line)
            if m:
                interface_name, method_name = m.groups()
                yield {
                    "type": "method",
                    "interface_name": interface_name,
                    "method_name": method_name,
                }
                continue
            m = re.match(r"attribute ([A-Za-z0-9]+)\.([A-Za-z0-9]+)$", line)
            if m:
                interface_name, attribute_name = m.groups()
                yield {
                    "type": "attribute",
                    "interface_name": interface_name,
                    "attribute_name": attribute_name,
                }
                continue
            m = re.match(r"custom ([A-Za-z0-9_]+) (.*)$", line)
            if m:
                name, desc = m.groups()
                yield {"type": "custom", "name": name, "desc": desc}
                continue
            raise ValueError(
                "error parsing %s at line %d" % (conf_filename, line_num + 1)
            )

    return parse_counters(stream)


YAML_HEADER = """\
# This file is AUTOGENERATED by usecounters.py. DO NOT EDIT.
# (instead, re-run ./mach gen-use-counter-metrics)

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

---
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
$tags:
  - 'Core :: DOM: Core & HTML'

"""

BASE_METRICS = """\
use.counter:
  content_documents_destroyed:
    type: counter
    description: >
      A count of how many content documents were destroyed.
      Used to turn document use counters' counts into rates.
      Excludes documents for which we do not count use counters
      (See `Document::ShouldIncludeInTelemetry`).
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1204994
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1845779
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters

  top_level_content_documents_destroyed:
    type: counter
    description: >
      A count of how many "pages" were destroyed.
      Used to turn page use counters' counts into rates.
      Excludes pages that contain only documents for which we do not count use
      counters (See `Document::ShouldIncludeInTelemetry`).
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1204994
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1845779
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1569672
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters
      - metrics

  dedicated_workers_destroyed:
    type: counter
    description: >
      A count of how many `Dedicated`-kind workers were destroyed.
      Used to turn dedicated worker use counters' counts into rates.
      Excludes chrome workers.
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters

  shared_workers_destroyed:
    type: counter
    description: >
      A count of how many `Shared`-kind workers were destroyed.
      Used to turn shared worker use counters' counts into rates.
      Excludes chrome workers.
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters

  service_workers_destroyed:
    type: counter
    description: >
      A count of how many `Service`-kind workers were destroyed.
      Used to turn service worker use counters' counts into rates.
      Excludes chrome workers.
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1202706
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters

"""

USE_COUNTER_TEMPLATE = """\
  {name}:
    type: counter
    description: >
      {desc}
      Compare against `{denominator}`
      to calculate the rate.
    bugs:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    data_reviews:
      - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098
    notification_emails:
      - dom-core@mozilla.com
      - emilio@mozilla.com
    expires: never
    send_in_pings:
      - use-counters

"""


def gen_use_counter_metrics():
    """
    Finds use counters in:
      * dom/base/UseCounters.conf
      * dom/base/UseCountersWorker.conf
      * dom/base/nsDeprecatedOperationsList.h
      * !/layout/style/ServoCSSPropList.py
      * servo/components/style/properties/counted_unknown_properties.py
    and overwrites the Glean metrics definition file
    `dom/base/use_counter_metrics.yaml` with definitions for each use counter found.

    IF YOU CHANGE THIS FUNCTION:
      * You should probably add your bug's number to USE_COUNTER_TEMPLATE, above.

    Returns 0 on success.
    """
    (
        page,
        doc,
        dedicated,
        shared,
        service,
        ops_page,
        ops_doc,
        css_page,
        css_doc,
    ) = parse_use_counters()
    import os

    import buildconfig
    from mozbuild.util import FileAvoidWrite

    yaml_path = os.path.join(
        buildconfig.topsrcdir, "dom", "base", "use_counter_metrics.yaml"
    )
    with FileAvoidWrite(yaml_path) as f:
        f.write(YAML_HEADER)
        f.write(BASE_METRICS)

        total = (
            len(page)
            + len(doc)
            + len(dedicated)
            + len(shared)
            + len(service)
            + len(ops_page)
            + len(ops_doc)
            + len(css_page)
            + len(css_doc)
        )
        f.write(f"# Total of {total} use counter metrics (excludes denominators).\n")
        f.write(f"# Total of {len(page)} 'page' use counters.\n")
        f.write("use.counter.page:\n")
        for [_, name, desc] in page:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.top_level_content_documents_destroyed",
                )
            )

        f.write(f"# Total of {len(doc)} 'document' use counters.\n")
        f.write("use.counter.doc:\n")
        for [_, name, desc] in doc:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.content_documents_destroyed",
                )
            )

        f.write(f"# Total of {len(dedicated)} 'dedicated worker' use counters.\n")
        f.write("use.counter.worker.dedicated:\n")
        for [_, name, desc] in dedicated:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.dedicated_workers_destroyed",
                )
            )

        f.write(f"# Total of {len(shared)} 'shared worker' use counters.\n")
        f.write("use.counter.worker.shared:\n")
        for [_, name, desc] in shared:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.shared_workers_destroyed",
                )
            )

        f.write(f"# Total of {len(service)} 'service worker' use counters.\n")
        f.write("use.counter.worker.service:\n")
        for [_, name, desc] in service:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.service_workers_destroyed",
                )
            )

        f.write(
            f"# Total of {len(ops_page)} 'deprecated operations (page)' use counters.\n"
        )
        f.write("use.counter.deprecated_ops.page:\n")
        for [_, name, desc] in ops_page:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.top_level_content_documents_destroyed",
                )
            )

        f.write(
            f"# Total of {len(ops_doc)} 'deprecated operations (document)' use counters.\n"
        )
        f.write("use.counter.deprecated_ops.doc:\n")
        for [_, name, desc] in ops_doc:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.content_documents_destroyed",
                )
            )

        f.write(f"# Total of {len(css_page)} 'CSS (page)' use counters.\n")
        f.write("use.counter.css.page:\n")
        for [_, name, desc] in css_page:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.top_level_content_documents_destroyed",
                )
            )

        f.write(f"# Total of {len(css_doc)} 'CSS (document)' use counters.\n")
        f.write("use.counter.css.doc:\n")
        for [_, name, desc] in css_doc:
            f.write(
                USE_COUNTER_TEMPLATE.format(
                    name=name,
                    desc=desc,
                    denominator="use.counter.content_documents_destroyed",
                )
            )

    return 0


def parse_use_counters():
    """
    Finds use counters in:
      * dom/base/UseCounters.conf
      * dom/base/UseCountersWorker.conf
      * dom/base/nsDeprecatedOperationsList.h
      * !/layout/style/ServoCSSPropList.py
      * servo/components/style/properties/counted_unknown_properties.py
    and returns them as a tuple of lists of tuples of the form:
    (page, doc, dedicated, shared, service, ops_page, ops_doc, css_page, css_doc)
    where each of the items is a List<Tuple<enum_name, glean_name, description>>
    where `enum_name` is the name of the enum variant from UseCounter.h
    (like `eUseCounter_custom_CustomizedBuiltin`), and
    where `glean_name` is the name conjugated to Glean metric name safety.
    """

    # Note, this function contains a duplication of enum naming logic from UseCounter.h.
    # If you change the enum name format, you'll need to do it twice.

    # There are 3 kinds of Use Counters in conf files: method, attribute, custom.
    # `method` and `attribute` are presumed label-safe and are taken as-is.
    # `custom` can be any case, so are coerced to snake_case.
    import os

    import buildconfig

    uc_path = os.path.join(buildconfig.topsrcdir, "dom", "base", "UseCounters.conf")
    page = []
    doc = []
    for counter in read_conf(uc_path):
        if counter["type"] == "method":
            enum_name = (
                f"eUseCounter_{counter['interface_name']}_{counter['method_name']}"
            )
            glean_name = f"{counter['interface_name']}_{counter['method_name']}".lower()
            method = f"called {counter['interface_name']}.{counter['method_name']}"
            page.append((enum_name, glean_name, f"Whether a page called {method}."))
            doc.append((enum_name, glean_name, f"Whether a document called {method}."))
        elif counter["type"] == "attribute":
            enum_root = (
                f"eUseCounter_{counter['interface_name']}_{counter['attribute_name']}"
            )
            name = f"{counter['interface_name']}_{counter['attribute_name']}".lower()
            attr = f"{counter['interface_name']}.{counter['attribute_name']}"
            page.append(
                (f"{enum_root}_getter", f"{name}_getter", f"Whether a page got {attr}.")
            )
            page.append(
                (f"{enum_root}_setter", f"{name}_setter", f"Whether a page set {attr}.")
            )
            doc.append(
                (
                    f"{enum_root}_getter",
                    f"{name}_getter",
                    f"Whether a document got {attr}.",
                )
            )
            doc.append(
                (
                    f"{enum_root}_setter",
                    f"{name}_setter",
                    f"Whether a document set {attr}.",
                )
            )
        elif counter["type"] == "custom":
            enum_name = f"eUseCounter_custom_{counter['name']}"
            page.append(
                (
                    enum_name,
                    to_snake_case(counter["name"]),
                    f"Whether a page {counter['desc']}.",
                )
            )
            doc.append(
                (
                    enum_name,
                    to_snake_case(counter["name"]),
                    f"Whether a document {counter['desc']}.",
                )
            )
        else:
            print(f"Found unexpected use counter type {counter['type']}. Returning 1.")
            return 1

    worker_uc_path = os.path.join(
        buildconfig.topsrcdir, "dom", "base", "UseCountersWorker.conf"
    )
    dedicated = []
    shared = []
    service = []
    for counter in read_conf(worker_uc_path):
        if counter["type"] == "method":
            enum_name = f"{counter['interface_name']}_{counter['method_name']}"
            name = f"{counter['interface_name']}_{counter['method_name']}".lower()
            method = f"called {counter['interface_name']}.{counter['method_name']}"
            dedicated.append(
                (enum_name, name, f"Whether a dedicated worker called {method}.")
            )
            shared.append(
                (enum_name, name, f"Whether a shared worker called {method}.")
            )
            service.append(
                (enum_name, name, f"Whether a service worker called {method}.")
            )
        elif counter["type"] == "attribute":
            enum_root = f"{counter['interface_name']}_{counter['attribute_name']}"
            name = f"{counter['interface_name']}_{counter['attribute_name']}".lower()
            attr = f"{counter['interface_name']}.{counter['attribute_name']}"
            dedicated.append(
                (
                    f"{enum_root}_getter",
                    f"{name}_getter",
                    f"Whether a dedicated worker got {attr}.",
                )
            )
            dedicated.append(
                (
                    f"{enum_root}_setter",
                    f"{name}_setter",
                    f"Whether a dedicated worker set {attr}.",
                )
            )
            shared.append(
                (
                    f"{enum_root}_getter",
                    f"{name}_getter",
                    f"Whether a shared worker got {attr}.",
                )
            )
            shared.append(
                (
                    f"{enum_root}_setter",
                    f"{name}_setter",
                    f"Whether a shared worker set {attr}.",
                )
            )
            service.append(
                (
                    f"{enum_root}_getter",
                    f"{name}_getter",
                    f"Whether a service worker got {attr}.",
                )
            )
            service.append(
                (
                    f"{enum_root}_setter",
                    f"{name}_setter",
                    f"Whether a service worker set {attr}.",
                )
            )
        elif counter["type"] == "custom":
            enum_name = f"Custom_{counter['name']}"
            dedicated.append(
                (
                    enum_name,
                    to_snake_case(counter["name"]),
                    f"Whether a dedicated worker {counter['desc']}.",
                )
            )
            shared.append(
                (
                    enum_name,
                    to_snake_case(counter["name"]),
                    f"Whether a shared worker {counter['desc']}.",
                )
            )
            service.append(
                (
                    enum_name,
                    to_snake_case(counter["name"]),
                    f"Whether a service worker {counter['desc']}.",
                )
            )
        else:
            print(
                f"Found unexpected worker use counter type {counter['type']}. Returning 1."
            )
            return 1

    # nsDeprecatedOperationsList.h parsing is adapted from parse_histograms.py.
    operation_list_path = os.path.join(
        buildconfig.topsrcdir, "dom", "base", "nsDeprecatedOperationList.h"
    )
    operation_regex = re.compile("^DEPRECATED_OPERATION\\(([^)]+)\\)")
    ops_page = []
    ops_doc = []
    with open(operation_list_path) as f:
        for line in f:
            match = operation_regex.search(line)
            if not match:
                # No macro, probably whitespace or comment.
                continue

            op = match.group(1)
            op_name = to_snake_case(op)
            enum_name = f"eUseCounter_{op}"
            ops_page.append((enum_name, op_name, f"Whether a page used {op}."))
            ops_doc.append((enum_name, op_name, f"Whether a document used {op}."))

    # Theoretically, we could do this without a completed build
    # (ie, without the generated ServoCSSPropList.py) by sourcing direct from
    # servo/components/style/properties/data.py:PropertiesData(engine=gecko).
    #
    # ...but parse_histograms.py doesn't do this the hard way. Should we?

    import runpy

    proplist_path = os.path.join(
        buildconfig.topobjdir, "layout", "style", "ServoCSSPropList.py"
    )
    css_properties = runpy.run_path(proplist_path)["data"]
    css_page = []
    css_doc = []
    for prop in css_properties.values():
        # We prefix `prop_name` with `css_` to avoid colliding with C++ keywords
        # like `float`.
        prop_name = "css_" + to_snake_case(prop.name)

        # Dependency keywords: CSS_PROP_PUBLIC_OR_PRIVATE, GenerateServoCSSPropList.py.
        method = "Float" if prop.method == "CssFloat" else prop.method
        # Dependency keywords: CSS_PROP_DOMPROP_PREFIXED, GenerateServoCSSPropList.py.
        if method.startswith("Moz") and prop.type() != "alias":
            method = method[3:]  # remove the moz prefix

        enum_name = f"eUseCounter_property_{method}"
        css_page.append(
            (enum_name, prop_name, f"Whether a page used the CSS property {prop.name}.")
        )
        css_doc.append(
            (
                enum_name,
                prop_name,
                f"Whether a document used the CSS property {prop.name}.",
            )
        )

    # Counted unknown properties: AKA - stuff that doesn't exist, but we want
    # to count uses of anyway.
    # We _might_ decide to implement these in the future, though, so we just add
    # them to the css_page, css_doc lists directly for continuity.
    # (We do give them a different description, though)

    import sys

    sys.path.append(os.path.join(buildconfig.topsrcdir, "layout", "style"))
    from GenerateCountedUnknownProperties import to_camel_case

    unknown_proplist_path = os.path.join(
        buildconfig.topsrcdir,
        "servo",
        "components",
        "style",
        "properties",
        "counted_unknown_properties.py",
    )
    unknown_properties = runpy.run_path(unknown_proplist_path)[
        "COUNTED_UNKNOWN_PROPERTIES"
    ]
    for prop in unknown_properties:
        enum_name = f"eUseCounter_unknown_property_{to_camel_case(prop)}"
        prop_name = to_snake_case(prop)
        css_page.append(
            (
                enum_name,
                prop_name,
                f"Whether a page used the (unknown, counted) CSS property {prop}.",
            )
        )
        css_doc.append(
            (
                enum_name,
                prop_name,
                f"Whether a document used the (unknown, counted) CSS property {prop}.",
            )
        )

    return (page, doc, dedicated, shared, service, ops_page, ops_doc, css_page, css_doc)


def to_snake_case(kebab_or_pascal):
    """
    Takes `kebab_or_pascal` which is in PascalCase or kebab-case
    and conjugates it to "snake_case" (all lowercase, "_"-delimited).
    """
    return (
        re.sub("([A-Z]+)", r"_\1", kebab_or_pascal).replace("-", "_").lower().strip("_")
    )


def metric_map(f, *inputs):
    """
    Parses all use counters and outputs UseCounter.cpp which contains implementations
    for two functions defined in UseCounter.h:
      * const char* IncrementUseCounter(UseCounter aUseCounter, bool aIsPage)
      * const char* IncrementWorkerUseCounter(UseCounterWorker aUseCounter, dom::WorkerKind aKind)

    (Basically big switch statements mapping from enums to glean metrics, calling Add())
    """

    (
        page,
        doc,
        dedicated,
        shared,
        service,
        ops_page,
        ops_doc,
        css_page,
        css_doc,
    ) = parse_use_counters()

    f.write(
        """\
/* AUTOGENERATED by usecounters.py. DO NOT EDIT */
/* 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/. */

#include "mozilla/dom/UseCounterMetrics.h"

#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/glean/DomUseCounterMetrics.h"

namespace mozilla::dom {

const char* IncrementUseCounter(UseCounter aUseCounter, bool aIsPage) {
  static constexpr struct {
    const char* name;
    glean::impl::CounterMetric<> doc_metric;
    glean::impl::CounterMetric<> page_metric;
  } kEntries[] = {
"""
    )

    # This order must match the order UseCounter is defined,
    # (and we guarantee it via the MOZ_ASSERT below at runtime).
    assert len(page) == len(doc)
    assert len(ops_page) == len(ops_doc)
    assert len(css_page) == len(css_doc)

    index = 0
    static_asserts = []
    for pc, dc in zip(page, doc):
        assert pc[0] == dc[0]
        assert pc[1] == dc[1]
        static_asserts.append(f"static_assert({index} == size_t(UseCounter::{pc[0]}));")
        f.write(
            f"""\
        {{
          "{pc[1]}",
          glean::use_counter_doc::{pc[1]},
          glean::use_counter_page::{pc[1]},
        }},
"""
        )
        index += 1

    for pc, dc in zip(ops_page, ops_doc):
        assert pc[0] == dc[0]
        assert pc[1] == dc[1]
        static_asserts.append(f"static_assert({index} == size_t(UseCounter::{pc[0]}));")
        f.write(
            f"""\
        {{
          "deprecated_ops.{pc[1]}",
          glean::use_counter_deprecated_ops_doc::{pc[1]},
          glean::use_counter_deprecated_ops_page::{pc[1]},
        }},
"""
        )
        index += 1

    for pc, dc in zip(css_page, css_doc):
        assert pc[0] == dc[0]
        assert pc[1] == dc[1]
        static_asserts.append(f"static_assert({index} == size_t(UseCounter::{pc[0]}));")
        f.write(
            f"""\
        {{
          "css.{pc[1]}",
          glean::use_counter_css_doc::{pc[1]},
          glean::use_counter_css_page::{pc[1]},
        }},
"""
        )
        index += 1

    f.write("};\n")
    f.write("\n".join(static_asserts))
    f.write(
        """\
  MOZ_ASSERT(size_t(aUseCounter) < std::size(kEntries));
  const auto& entry = kEntries[size_t(aUseCounter)];
  (aIsPage ? entry.page_metric : entry.doc_metric).Add();
  return entry.name;
}

const char* IncrementWorkerUseCounter(UseCounterWorker aUseCounter, WorkerKind aKind) {
  static constexpr struct {
    const char* name;
    glean::impl::CounterMetric<> dedicated_metric;
    glean::impl::CounterMetric<> shared_metric;
    glean::impl::CounterMetric<> service_metric;
  } kEntries[] = {
"""
    )
    assert len(dedicated) == len(shared)
    assert len(dedicated) == len(service)
    index = 0
    static_asserts = []
    for dc, sc, servicec in zip(dedicated, shared, service):
        assert dc[0] == sc[0]
        assert dc[1] == sc[1]
        assert dc[0] == servicec[0]
        assert dc[1] == servicec[1]
        static_asserts.append(
            f"static_assert({index} == size_t(UseCounterWorker::{dc[0]}));"
        )
        f.write(
            f"""\
        {{
          "{dc[1]}",
          glean::use_counter_worker_dedicated::{dc[1]},
          glean::use_counter_worker_shared::{dc[1]},
          glean::use_counter_worker_service::{dc[1]},
        }},
"""
        )
        index += 1
    f.write("};\n")
    f.write("\n".join(static_asserts))
    f.write(
        """\
  MOZ_ASSERT(size_t(aUseCounter) < std::size(kEntries));
  const auto& entry = kEntries[size_t(aUseCounter)];
  switch (aKind) {
    case WorkerKind::WorkerKindDedicated:
      entry.dedicated_metric.Add();
      break;
    case WorkerKind::WorkerKindShared:
      entry.shared_metric.Add();
      break;
    case WorkerKind::WorkerKindService:
      entry.service_metric.Add();
      break;
  }
  return entry.name;
}

}  // namespace mozilla
"""
    )
