from __future__ import annotations

import json
from typing import Any, Literal

import jinja2

from altair.utils._importers import import_vl_convert, vl_version_for_vl_convert

TemplateName = Literal["standard", "universal", "inline", "olli"]
RenderMode = Literal["vega", "vega-lite"]

HTML_TEMPLATE = jinja2.Template(
    """
{%- if fullhtml -%}
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
{%- endif %}
  <style>
    #{{ output_div }}.vega-embed {
      width: 100%;
      display: flex;
    }

    #{{ output_div }}.vega-embed details,
    #{{ output_div }}.vega-embed details summary {
      position: relative;
    }
  </style>
{%- if not requirejs %}
  <script type="text/javascript" src="{{ base_url }}/vega@{{ vega_version }}"></script>
  {%- if mode == 'vega-lite' %}
  <script type="text/javascript" src="{{ base_url }}/vega-lite@{{ vegalite_version }}"></script>
  {%- endif %}
  <script type="text/javascript" src="{{ base_url }}/vega-embed@{{ vegaembed_version }}"></script>
{%- endif %}
{%- if fullhtml %}
{%- if requirejs %}
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
requirejs.config({
    "paths": {
        "vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
        "vega-lib": "{{ base_url }}/vega-lib?noext",
        "vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
        "vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
    }
});
</script>
{%- endif %}
</head>
<body>
{%- endif %}
  <div id="{{ output_div }}"></div>
  <script>
    {%- if requirejs and not fullhtml %}
    requirejs.config({
        "paths": {
            "vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
            "vega-lib": "{{ base_url }}/vega-lib?noext",
            "vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
            "vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
        }
    });
    {% endif %}
    {% if requirejs -%}
    require(['vega-embed'],
    {%- else -%}
    (
    {%- endif -%}
    function(vegaEmbed) {
      var spec = {{ spec }};
      var embedOpt = {{ embed_options }};

      function showError(el, error){
          el.innerHTML = ('<div style="color:red;">'
                          + '<p>JavaScript Error: ' + error.message + '</p>'
                          + "<p>This usually means there's a typo in your chart specification. "
                          + "See the javascript console for the full traceback.</p>"
                          + '</div>');
          throw error;
      }
      const el = document.getElementById('{{ output_div }}');
      vegaEmbed("#{{ output_div }}", spec, embedOpt)
        .catch(error => showError(el, error));
    }){% if not requirejs %}(vegaEmbed){% endif %};

  </script>
{%- if fullhtml %}
</body>
</html>
{%- endif %}
"""
)


HTML_TEMPLATE_UNIVERSAL = jinja2.Template(
    """
<style>
  #{{ output_div }}.vega-embed {
    width: 100%;
    display: flex;
  }

  #{{ output_div }}.vega-embed details,
  #{{ output_div }}.vega-embed details summary {
    position: relative;
  }
</style>
<div id="{{ output_div }}"></div>
<script type="text/javascript">
  var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
  (function(spec, embedOpt){
    let outputDiv = document.currentScript.previousElementSibling;
    if (outputDiv.id !== "{{ output_div }}") {
      outputDiv = document.getElementById("{{ output_div }}");
    }

    const paths = {
      "vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
      "vega-lib": "{{ base_url }}/vega-lib?noext",
      "vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
      "vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
    };

    function maybeLoadScript(lib, version) {
      var key = `${lib.replace("-", "")}_version`;
      return (VEGA_DEBUG[key] == version) ?
        Promise.resolve(paths[lib]) :
        new Promise(function(resolve, reject) {
          var s = document.createElement('script');
          document.getElementsByTagName("head")[0].appendChild(s);
          s.async = true;
          s.onload = () => {
            VEGA_DEBUG[key] = version;
            return resolve(paths[lib]);
          };
          s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
          s.src = paths[lib];
        });
    }

    function showError(err) {
      outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
      throw err;
    }

    function displayChart(vegaEmbed) {
      vegaEmbed(outputDiv, spec, embedOpt)
        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));
    }

    if(typeof define === "function" && define.amd) {
      requirejs.config({paths});
      let deps = ["vega-embed"];
      require(deps, displayChart, err => showError(`Error loading script: ${err.message}`));
    } else {
      maybeLoadScript("vega", "{{vega_version}}")
        .then(() => maybeLoadScript("vega-lite", "{{vegalite_version}}"))
        .then(() => maybeLoadScript("vega-embed", "{{vegaembed_version}}"))
        .catch(showError)
        .then(() => displayChart(vegaEmbed));
    }
  })({{ spec }}, {{ embed_options }});
</script>
"""
)


# This is like the HTML_TEMPLATE template, but includes vega javascript inline
# so that the resulting file is not dependent on external resources. This was
# ported over from altair_saver.
#
# implies requirejs=False and full_html=True
INLINE_HTML_TEMPLATE = jinja2.Template(
    """\
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <style>
    #{{ output_div }}.vega-embed {
      width: 100%;
      display: flex;
    }

    #{{ output_div }}.vega-embed details,
    #{{ output_div }}.vega-embed details summary {
      position: relative;
    }
  </style>
  <script type="text/javascript">
    // vega-embed.js bundle with Vega-Lite version v{{ vegalite_version }}
    {{ vegaembed_script }}
  </script>
</head>
<body>
<div class="vega-visualization" id="{{ output_div }}"></div>
<script type="text/javascript">
  const spec = {{ spec }};
  const embedOpt = {{ embed_options }};
  vegaEmbed('#{{ output_div }}', spec, embedOpt).catch(console.error);
</script>
</body>
</html>
"""
)


HTML_TEMPLATE_OLLI = jinja2.Template(
    """
<style>
  #{{ output_div }}.vega-embed {
    width: 100%;
    display: flex;
  }

  #{{ output_div }}.vega-embed details,
  #{{ output_div }}.vega-embed details summary {
    position: relative;
  }
</style>
<div id="{{ output_div }}"></div>
<script type="text/javascript">
  var VEGA_DEBUG = (typeof VEGA_DEBUG == "undefined") ? {} : VEGA_DEBUG;
  (function(spec, embedOpt){
    let outputDiv = document.currentScript.previousElementSibling;
    if (outputDiv.id !== "{{ output_div }}") {
      outputDiv = document.getElementById("{{ output_div }}");
    }
    const olliDiv = document.createElement("div");
    const vegaDiv = document.createElement("div");
    outputDiv.appendChild(vegaDiv);
    outputDiv.appendChild(olliDiv);
    outputDiv = vegaDiv;

    const paths = {
      "vega": "{{ base_url }}/vega@{{ vega_version }}?noext",
      "vega-lib": "{{ base_url }}/vega-lib?noext",
      "vega-lite": "{{ base_url }}/vega-lite@{{ vegalite_version }}?noext",
      "vega-embed": "{{ base_url }}/vega-embed@{{ vegaembed_version }}?noext",
      "olli": "{{ base_url }}/olli@{{ olli_version }}?noext",
      "olli-adapters": "{{ base_url }}/olli-adapters@{{ olli_adapters_version }}?noext",
    };

    function maybeLoadScript(lib, version) {
      var key = `${lib.replace("-", "")}_version`;
      return (VEGA_DEBUG[key] == version) ?
        Promise.resolve(paths[lib]) :
        new Promise(function(resolve, reject) {
          var s = document.createElement('script');
          document.getElementsByTagName("head")[0].appendChild(s);
          s.async = true;
          s.onload = () => {
            VEGA_DEBUG[key] = version;
            return resolve(paths[lib]);
          };
          s.onerror = () => reject(`Error loading script: ${paths[lib]}`);
          s.src = paths[lib];
        });
    }

    function showError(err) {
      outputDiv.innerHTML = `<div class="error" style="color:red;">${err}</div>`;
      throw err;
    }

    function displayChart(vegaEmbed, olli, olliAdapters) {
      vegaEmbed(outputDiv, spec, embedOpt)
        .catch(err => showError(`Javascript Error: ${err.message}<br>This usually means there's a typo in your chart specification. See the javascript console for the full traceback.`));
      olliAdapters.VegaLiteAdapter(spec).then(olliVisSpec => {
        const olliFunc = typeof olli === 'function' ? olli : olli.olli;
        const olliRender = olliFunc(olliVisSpec);
        olliDiv.append(olliRender);
      });
    }

    if(typeof define === "function" && define.amd) {
      requirejs.config({paths});
      let deps = ["vega-embed", "olli", "olli-adapters"];
      require(deps, displayChart, err => showError(`Error loading script: ${err.message}`));
    } else {
      maybeLoadScript("vega", "{{vega_version}}")
        .then(() => maybeLoadScript("vega-lite", "{{vegalite_version}}"))
        .then(() => maybeLoadScript("vega-embed", "{{vegaembed_version}}"))
        .then(() => maybeLoadScript("olli", "{{olli_version}}"))
        .then(() => maybeLoadScript("olli-adapters", "{{olli_adapters_version}}"))
        .catch(showError)
        .then(() => displayChart(vegaEmbed, olli, OlliAdapters));
    }
  })({{ spec }}, {{ embed_options }});
</script>
"""
)


TEMPLATES: dict[TemplateName, jinja2.Template] = {
    "standard": HTML_TEMPLATE,
    "universal": HTML_TEMPLATE_UNIVERSAL,
    "inline": INLINE_HTML_TEMPLATE,
    "olli": HTML_TEMPLATE_OLLI,
}


def spec_to_html(
    spec: dict[str, Any],
    mode: RenderMode,
    vega_version: str | None,
    vegaembed_version: str | None,
    vegalite_version: str | None = None,
    base_url: str = "https://cdn.jsdelivr.net/npm",
    output_div: str = "vis",
    embed_options: dict[str, Any] | None = None,
    json_kwds: dict[str, Any] | None = None,
    fullhtml: bool = True,
    requirejs: bool = False,
    template: jinja2.Template | TemplateName = "standard",
) -> str:
    """
    Embed a Vega/Vega-Lite spec into an HTML page.

    Parameters
    ----------
    spec : dict
        a dictionary representing a vega-lite plot spec.
    mode : string {'vega' | 'vega-lite'}
        The rendering mode. This value is overridden by embed_options['mode'],
        if it is present.
    vega_version : string
        For html output, the version of vega.js to use.
    vegalite_version : string
        For html output, the version of vegalite.js to use.
    vegaembed_version : string
        For html output, the version of vegaembed.js to use.
    base_url : string (optional)
        The base url from which to load the javascript libraries.
    output_div : string (optional)
        The id of the div element where the plot will be shown.
    embed_options : dict (optional)
        Dictionary of options to pass to the vega-embed script. Default
        entry is {'mode': mode}.
    json_kwds : dict (optional)
        Dictionary of keywords to pass to json.dumps().
    fullhtml : boolean (optional)
        If True (default) then return a full html page. If False, then return
        an HTML snippet that can be embedded into an HTML page.
    requirejs : boolean (optional)
        If False (default) then load libraries from base_url using <script>
        tags. If True, then load libraries using requirejs
    template : jinja2.Template or string (optional)
        Specify the template to use (default = 'standard'). If template is a
        string, it must be one of {'universal', 'standard', 'inline'}. Otherwise, it
        can be a jinja2.Template object containing a custom template.

    Returns
    -------
    output : string
        an HTML string for rendering the chart.
    """
    embed_options = embed_options or {}
    json_kwds = json_kwds or {}

    mode = embed_options.setdefault("mode", mode)

    if mode not in {"vega", "vega-lite"}:
        msg = "mode must be either 'vega' or 'vega-lite'"
        raise ValueError(msg)

    if vega_version is None:
        msg = "must specify vega_version"
        raise ValueError(msg)

    if vegaembed_version is None:
        msg = "must specify vegaembed_version"
        raise ValueError(msg)

    if mode == "vega-lite" and vegalite_version is None:
        msg = "must specify vega-lite version for mode='vega-lite'"
        raise ValueError(msg)

    render_kwargs = {}
    if template == "inline":
        vlc = import_vl_convert()
        vl_version = vl_version_for_vl_convert()
        render_kwargs["vegaembed_script"] = vlc.javascript_bundle(vl_version=vl_version)
    elif template == "olli":
        OLLI_VERSION = "2"
        OLLI_ADAPTERS_VERSION = "2"
        render_kwargs["olli_version"] = OLLI_VERSION
        render_kwargs["olli_adapters_version"] = OLLI_ADAPTERS_VERSION

    jinja_template = TEMPLATES.get(template, template)  # type: ignore[arg-type]
    if not hasattr(jinja_template, "render"):
        msg = f"Invalid template: {jinja_template}"
        raise ValueError(msg)

    return jinja_template.render(
        spec=json.dumps(spec, **json_kwds),
        embed_options=json.dumps(embed_options),
        mode=mode,
        vega_version=vega_version,
        vegalite_version=vegalite_version,
        vegaembed_version=vegaembed_version,
        base_url=base_url,
        output_div=output_div,
        fullhtml=fullhtml,
        requirejs=requirejs,
        **render_kwargs,
    )
