from __future__ import annotations

import json
from collections import OrderedDict

from .. import config, h, messages as m, t
from ..translate import _

if t.TYPE_CHECKING:

    class MdnFeatureT(t.TypedDict, total=False):
        engines: list[str]
        filename: str
        name: str
        slug: str
        summary: str
        mdn_url: str
        support: dict[str, t.Any]
        # the support values are wild

    MdnDataT: t.TypeAlias = dict[str, list[MdnFeatureT]]


def addMdnPanels(doc: t.SpecT) -> None:
    if not doc.md.includeMdnPanels:
        return

    try:
        filename = f"{doc.md.vshortname}.json"
        datafile = doc.dataFile.fetch("mdn", filename, str=True)
    except OSError:
        try:
            filename = f"{doc.md.shortname}.json"
            datafile = doc.dataFile.fetch("mdn", filename, str=True)
        except OSError:
            if doc.md.includeMdnPanels == "maybe":
                # if "maybe", failure is fine, don't complain
                pass
            else:
                m.die(f"Couldn't find the MDN data for '{doc.md.vshortname}' nor '{doc.md.shortname}'.")
            return
    try:
        data = t.cast("MdnDataT", json.loads(datafile, object_pairs_hook=OrderedDict))
    except Exception as e:
        m.die(f"Couldn't load MDN Spec Links data for this spec.\n{e}")
        return

    panels = panelsFromData(doc, data)
    if panels:
        doc.extraScripts["script-mdn-anno"] = getModuleFile("mdn.js")
        doc.extraStyles["style-mdn-anno"] = getModuleFile("mdn.css")
        doc.extraStyles["style-darkmode"] += getModuleFile("mdn-dark.css")


def createAnno(className: str, mdnButton: t.ElementT, featureDivs: list[t.ElementT]) -> t.ElementT:
    return h.E.div({"class": className}, mdnButton, featureDivs)


def panelsFromData(doc: t.SpecT, data: MdnDataT) -> bool:
    mdnBaseUrl = "https://developer.mozilla.org/en-US/docs/Web/"

    browsersProvidingCurrentEngines = ["firefox", "safari", "chrome"]
    browsersWithBorrowedEngines = ["opera", "edge_blink"]
    browsersWithRetiredEngines = ["edge", "ie"]
    browsersForMobileDevices = [
        "firefox_android",
        "safari_ios",
        "chrome_android",
        "webview_android",
        "samsunginternet_android",
        "opera_android",
    ]

    # BCD/mdn-spec-links shortnames to full names
    nameFromCodeName = {
        "chrome": "Chrome",
        "chrome_android": "Chrome for Android",
        "edge": "Edge (Legacy)",
        "edge_blink": "Edge",
        "firefox": "Firefox",
        "firefox_android": "Firefox for Android",
        "ie": "IE",
        "nodejs": "Node.js",
        "opera": "Opera",
        "opera_android": "Opera Mobile",
        "safari": "Safari",
        "samsunginternet_android": "Samsung Internet",
        "safari_ios": "iOS Safari",
        "webview_android": "Android WebView",
    }

    panels = False
    for elementId, features in data.items():
        isAnnoForHeadingContent = False
        isAnnoForListItemOrTableContent = False
        lessThanTwoEngines = 0
        onlyTwoEngines = 0
        allEngines = 0
        featureDivs = []
        targetElement = h.find(f"[id='{elementId}']", doc)
        if targetElement is None:
            m.warn(f"No '{elementId}' ID found, skipping MDN features that would target it.")
            continue

        panels = True
        if targetElement.tag in ["h1", "h2", "h3", "h4", "h5", "h6"]:
            isAnnoForHeadingContent = True
        else:
            for ancestor in targetElement.iterancestors():
                if ancestor.tag in [
                    "body",
                    "main",
                    "article",
                    "aside",
                    "nav",
                    "section",
                    "header",
                    "footer",
                ]:
                    break
                targetElement = ancestor
                if ancestor.tag in ["h1", "h2", "h3", "h4", "h5", "h6"]:
                    isAnnoForHeadingContent = True
                    break
                if ancestor.tag in ["td", "dt", "dd", "li"]:
                    isAnnoForListItemOrTableContent = True
                    break
                if ancestor.tag in ["pre", "xmp", "p"]:
                    break
        for feature in features:
            if "engines" in feature:
                engines = len(feature["engines"])
                if engines < 2:
                    lessThanTwoEngines = lessThanTwoEngines + 1
                elif engines == 2:
                    onlyTwoEngines = onlyTwoEngines + 1
                elif engines >= len(browsersProvidingCurrentEngines):
                    allEngines = allEngines + 1
            featureDivs.append(
                mdnPanelFor(
                    feature,
                    mdnBaseUrl,
                    nameFromCodeName,
                    browsersProvidingCurrentEngines,
                    browsersWithBorrowedEngines,
                    browsersWithRetiredEngines,
                    browsersForMobileDevices,
                )
            )

        mdnButton = h.E.button({"class": "mdn-anno-btn"})
        if lessThanTwoEngines > 0:
            h.appendChild(
                mdnButton,
                h.E.b(
                    {
                        "class": "less-than-two-engines-flag",
                        "title": _("This feature is in less than two current engines."),
                    },
                    "\u26A0",
                ),
            )
        elif allEngines > 0 and lessThanTwoEngines == 0 and onlyTwoEngines == 0:
            h.appendChild(
                mdnButton,
                h.E.b(
                    {
                        "class": "all-engines-flag",
                        "title": _("This feature is in all current engines."),
                    },
                    "\u2714",
                ),
            )
        h.appendChild(mdnButton, h.E.span("MDN"))

        if isAnnoForListItemOrTableContent:
            firstChild = None
            if h.hasChildElements(targetElement):
                firstChild = list(h.childElements(targetElement))[0]
            if (
                firstChild is not None
                and h.hasClass(doc, firstChild, "mdn-anno")
                and not h.hasClass(doc, firstChild, "after")
            ):
                # If there's already an annotation at the point where we want
                # this, just re-use it (instead of creating another one).
                h.appendChild(firstChild, featureDivs)
            else:
                # For elements we're annotating inside a dt, dd, li, or td, we
                # prepend the annotation to the dt, dd, li, or td — because in
                # cases where we have a long table or list, all the annotations
                # for everything in it otherwise ends up being merged into a
                # single annotation way up at the top of the table or list.
                h.prependChild(targetElement, createAnno("mdn-anno wrapped", mdnButton, featureDivs))
        elif isAnnoForHeadingContent:
            nextEl = targetElement.getnext()
            if nextEl is not None and h.hasClass(doc, nextEl, "mdn-anno") and h.hasClass(doc, nextEl, "after"):
                # If there's already an "after" annotation
                # at the point where we want this,
                # just re-use it (instead of creating another one).
                h.appendChild(nextEl, featureDivs)
            else:
                # For elements we're annotating inside an h1-h6 heading, we
                # insert the annotation as the next sibling of the heading.
                h.insertAfter(targetElement, createAnno("mdn-anno wrapped after", mdnButton, featureDivs))
        else:
            prevEl = targetElement.getprevious()
            if prevEl is not None and h.hasClass(doc, prevEl, "mdn-anno") and not h.hasClass(doc, prevEl, "after"):
                # If there's already an annotation at the point where we want
                # this, just re-use it (instead of creating another one) —
                # unless it's a class=after annotation (following a heading).
                h.appendChild(prevEl, featureDivs)
            else:
                # For elements we're annotating that aren't inside a table or
                # list or heading, we insert the annotation as the previous
                # sibling of whatever block-level element holds the element.
                h.insertBefore(targetElement, createAnno("mdn-anno wrapped", mdnButton, featureDivs))
    return panels


def addSupportRow(
    browserCodeName: str,
    nameFromCodeName: dict[str, str],
    support: dict[str, t.Any],
    supportData: t.ElementT,
) -> None:
    if browserCodeName not in support:
        return
    isEdgeLegacy = browserCodeName == "edge"
    isIE = browserCodeName == "ie"
    needsFlag = False
    versionAdded = None
    versionRemoved = None
    thisBrowserSupport = support[browserCodeName]
    if isinstance(thisBrowserSupport, dict):
        if "version_added" in thisBrowserSupport:
            versionAdded = thisBrowserSupport["version_added"]
            if "flags" in thisBrowserSupport:
                needsFlag = True
            if (
                "prefix" in thisBrowserSupport
                or "alternative_name" in thisBrowserSupport
                or "partial_implementation" in thisBrowserSupport
            ):
                versionAdded = False
        if "version_removed" in thisBrowserSupport:
            versionRemoved = thisBrowserSupport["version_removed"]
    if isinstance(thisBrowserSupport, list):
        for versionDetails in thisBrowserSupport:
            if "version_removed" in versionDetails:
                versionRemoved = versionDetails["version_removed"]
                continue
            if "version_added" in versionDetails:
                if versionDetails["version_added"] is False:
                    versionAdded = False
                    continue
                if versionDetails["version_added"] is None:
                    versionAdded = None
                    continue
                if (
                    "prefix" in versionDetails
                    or "alternative_name" in versionDetails
                    or "partial_implementation" in versionDetails
                ):
                    continue
                if "flags" in thisBrowserSupport:
                    needsFlag = True
                versionAdded = versionDetails["version_added"]
                versionRemoved = None
                break
    statusCode = "n"
    if versionAdded is None:
        minVersion = "?"
    elif versionAdded is False:
        minVersion = "None"
    elif versionAdded is True:
        minVersion = "Yes"
        statusCode = "y"
    else:
        if versionRemoved is None:
            statusCode = "y"
            minVersion = versionAdded + "+"
            if isEdgeLegacy and versionAdded == "18":
                minVersion = "18"
            if isIE and versionAdded == "11":
                minVersion = "11"
        else:
            statusCode = "n"
            if versionAdded is not None:
                minVersion = versionAdded + "\u2013" + versionRemoved
            else:
                minVersion = "None"
    browserFullName = nameFromCodeName[browserCodeName]
    h.appendChild(
        supportData,
        browserCompatSpan(browserCodeName, browserFullName, statusCode, minVersion, needsFlag),
    )


def mdnPanelFor(
    feature: MdnFeatureT,
    mdnBaseUrl: str,
    nameFromCodeName: dict[str, str],
    browsersProvidingCurrentEngines: list[str],
    browsersWithBorrowedEngines: list[str],
    browsersWithRetiredEngines: list[str],
    browsersForMobileDevices: list[str],
) -> t.ElementT:
    featureDiv = h.E.div({"class": "feature"})
    if "slug" in feature:
        slug = feature["slug"]
        displaySlug = slug.split("/", 1)[1]
        title = feature.get("summary", "")
        mdnURL = mdnBaseUrl + slug
        h.appendChild(featureDiv, h.E.p({}, h.E.a({"href": mdnURL, "title": title}, displaySlug)))
    if "engines" in feature:
        engines = len(feature["engines"])
        enginesPara = None
        if engines == 0:
            enginesPara = h.E.p({"class": "less-than-two-engines-text"}, _("In no current engines."))
        elif engines == 1:
            enginesPara = h.E.p({"class": "less-than-two-engines-text"}, _("In only one current engine."))
        elif engines >= len(browsersProvidingCurrentEngines):
            enginesPara = h.E.p({"class": "all-engines-text"}, _("In all current engines."))
        if enginesPara is not None:
            h.appendChild(featureDiv, enginesPara)
    supportData = h.E.div({"class": "support"})
    h.appendChild(featureDiv, supportData)
    support = feature["support"]
    for browserCodeName in browsersProvidingCurrentEngines:
        addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
    h.appendChild(supportData, h.E.hr())
    for browserCodeName in browsersWithBorrowedEngines:
        addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
    h.appendChild(supportData, h.E.hr())
    for browserCodeName in browsersWithRetiredEngines:
        addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
    h.appendChild(supportData, h.E.hr())
    for browserCodeName in browsersForMobileDevices:
        addSupportRow(browserCodeName, nameFromCodeName, support, supportData)
    if "nodejs" in support:
        h.appendChild(supportData, h.E.hr())
        addSupportRow("nodejs", nameFromCodeName, support, supportData)
    return featureDiv


def browserCompatSpan(
    browserCodeName: str, browserFullName: str, statusCode: str, minVersion: str, needsFlag: bool
) -> t.ElementT:
    # browserCodeName: e.g. "chrome"
    # browserFullName: e.g. "Chrome for Android"
    minVersionAttributes = {}
    flagSymbol = ""
    if needsFlag:
        flagSymbol = "\U0001f530 "
        minVersionAttributes["title"] = _("Requires setting a user preference or runtime flag.")
    statusClass = {"y": "yes", "n": "no"}[statusCode]
    outer = h.E.span({"class": browserCodeName + " " + statusClass})
    h.appendChild(outer, h.E.span({}, browserFullName))
    h.appendChild(outer, h.E.span(minVersionAttributes, flagSymbol + minVersion))
    return outer


def getModuleFile(filename: str) -> str:
    with open(config.scriptPath("mdn", filename), "r", encoding="utf-8") as fh:
        return fh.read()
