#
# Copyright (c) 2021. JetBrains s.r.o.
# Use of this source code is governed by the MIT license that can be found in the LICENSE file.
#

"""A sphinx extension for inserting references in two formats - image or text.

The ``extref`` directive do the all job.

For example:

.. code-block:: rst

    .. extref:: name
        :type: image
        :url: https://example.com
        :image: default
        :width: 400

It approximately translates to the following html:

.. code-block:: html

    <a class="reference external image-reference" href="https://example.com">
        <img alt="default-text" title="default-text" src="default-image" width="400">
    </a>

Here ``alt``, ``title`` and ``src`` values comes from the JSON configuration file, and they implicitly specified by the ``name`` value.
The first one is given by default, the second is defined through the ``:image:`` option.

Configuration
-------------
In your configuration file add ``extref`` to your extensions list, e.g.:

.. code-block:: python

    extensions = [
        ...
        'extref',
        ...
    ]

The extension provides the following configuration values:

- ``extref_conf`` : str
    Path to the JSON configuration file with parameters variety for each reference.
    The structure is the following: {"name1": options, "name2": options, ...}.
    Here for each named reference there is a standard bunch of options that is described below in the "JSON Options" section.
    The name of the reference connects directive with this options.
- ``extref_images_dir`` : str
    Path to the output directory with images inside the documentation site.
- ``extref_logo_images`` : dict
    For each ``:ref:`` value that is used to be a logo it should be specified the path to the corresponding logo image.
- ``extref_default_type`` : {'image', 'logo', 'text'}
    Default ``:type:`` value if it is not specified.
- ``extref_default_ref`` : str
    Default ``:ref:`` value if it is not specified.
- ``extref_default_image`` : str
    Default ``:image:`` value if it is not specified.
- ``extref_class`` : str
    Additional custom class for the link tag.

JSON Options
------------
- ``ref`` :
    Dictionary of pairs ``"ref_type": "url"``.
    ``:ref: ref_type`` in the reStructuredText means ``href="url"`` in html.
    By default, if ``:ref:`` option and ``extref_default_ref`` config value are not specified, used the first reference among all.
- ``image`` :
    Dictionary of pairs ``"img_type": "path"``.
    ``:image: img_type`` in the reStructuredText means ``src="path"`` in html, if reference type is image.
    By default, if ``:image:`` option and ``extref_default_image`` config value are not specified, used the first image among all.
- ``title`` :
    String with default title value, if reference type is image.
- ``text`` :
    String with default text value.
    In html it means ``<a ...>text</a>`` when reference type is text and ``alt="text"`` in other cases.
    Used when ``:text:`` option is not specified.

Directive Options
-----------------
- ``type`` :
    Should be the one of the three types of references: text, image, logo.
- ``ref`` :
    Reference type from the JSON configuration file.
- ``url`` :
    Explicit url for the reference, that used instead of the ``:ref:`` option.
- ``image`` :
    Image type from the JSON configuration file.
- ``text`` :
    Explicit text for the reference.
- ``title`` :
    Explicit title for the reference.
- ``width`` :
    Width of the image if the ``:type:`` value is image or logo.
- ``height`` :
    Height of the image if the ``:type:`` value is image or logo.

Examples
--------
Suppose that ``extref_logo_images`` configuration value is the following:

.. code-block:: python

    extref_logo_images = {
        ...
        'kaggle': "_static/images/kaggle.svg",
        ...
    }

Suppose that JSON configuration file is the following:

.. code-block:: javascript

    {
        ...
        "example1": {
            "ref": {
                "nbviewer": "https://nbviewer.jupyter.org/github/Example/example/blob/master/example/example.ipynb",
                "kaggle": "https://www.kaggle.com/example/example"
            },
            "image": {
                "default": "_static/images/example1.png"
            },
            "text": "My Example"
        }
        ...
    }

Then

.. code-block:: rst

    .. extref:: example1

gives the following html:

.. code-block:: html

    <a class="reference external image-reference" href="https://nbviewer.jupyter.org/github/Example/example/blob/master/example/example.ipynb">
        <img alt="My Example" title="My Example" src="_static/images/example1.png">
    </a>

The code

.. code-block:: rst

    .. extref:: example1
        :type: logo
        :ref: kaggle

gives the following html:

.. code-block:: html

    <a class="reference external image-reference" href="https://www.kaggle.com/example/example">
        <img alt="My Example" title="My Example" src="_static/images/kaggle.svg">
    </a>

The code

.. code-block:: rst

    .. extref:: example1
        :type: text
        :url: https://example.com

gives the following html:

.. code-block:: html

    <a class="reference external" href="https://example.com">
        My Example
    </a>

"""

import os
import json
import shutil
from argparse import ArgumentParser
from urllib.parse import urlparse

from docutils import nodes
from docutils.parsers.rst import Directive, directives

REF_TYPES = ('image', 'logo', 'text')
IMAGES_DIR = "_extref_images"
LOGO_DIR = "logo"
AVAILABLE_UTILS = ["check_using_notebook_names"]

class ExtRefDirective(Directive):
    has_content = True
    option_spec = {
        'type': lambda t: directives.choice(t, REF_TYPES),
        'ref': directives.unchanged,
        'url': directives.uri,
        'image': directives.unchanged,
        'title': directives.unchanged,
        'text': directives.unchanged,
        'width': directives.unchanged,
        'height': directives.unchanged,
    }

    def run(self):
        return [nodes.raw(
            "",
            '<a class="{0}" href="{1}">{2}</a>'.format(self._class(), self._href(), self._content()),
            format='html'
        )]

    def _env(self):
        return self.state.document.settings.env

    def _conf(self):
        return self._env().config['extref_conf'][self.content[0]]

    def _type(self):
        if 'type' in self.options.keys():
            return self.options['type']
        return self._env().config['extref_default_type']

    def _href(self):
        if 'url' in self.options.keys():
            return self.options['url']
        return self._ref()

    def _ref(self):
        return self._conf()['ref'][self._ref_type()]

    def _ref_type(self):
        if 'ref' in self.options.keys():
            return self.options['ref']
        if 'extref_default_ref' in self._env().config and \
           self._env().config['extref_default_ref'] in self._conf()['ref']:
            return self._env().config['extref_default_ref']
        return list(self._conf()['ref'])[0]

    def _class(self):
        class_text = "reference {0}".format(self._url_type())
        if self._type() == 'image':
            class_text += " image-reference preview"
        if self._type() == 'logo':
            class_text += " image-reference logo"
        if self._custom_class() is not None:
            class_text += " {0}".format(self._custom_class())
        return class_text

    def _custom_class(self):
        if 'extref_class' in self._env().config:
            return self._env().config['extref_class']
        else:
            return None

    def _url_type(self):
        return "external" if urlparse(self._href()).netloc else "internal"

    def _content(self):
        if self._type() == 'image':
            return self._image()
        if self._type() == 'logo':
            return self._logo()
        return self._text()

    def _image(self):
        image_src_path = self._image_src_path()
        image_src_fullpath = os.path.join(self._env().app.srcdir, image_src_path)
        image_doc_path = os.path.join(self._env().config['extref_images_dir'], os.path.basename(image_src_path))
        image_doc_fullpath = os.path.join(self._env().app.outdir, image_doc_path)
        if not os.path.isfile(image_doc_fullpath):
            shutil.copy(image_src_fullpath, image_doc_fullpath)
        return self._image_tag(image_doc_path)

    def _image_src_path(self):
        conf_image = self._conf()['image']
        if 'image' in self.options.keys():
            return conf_image[self.options['image']]
        if 'extref_default_image' in self._env().config and \
           self._env().config['extref_default_image'] in conf_image:
            return conf_image[self._env().config['extref_default_image']]
        return conf_image[list(conf_image)[0]]

    def _alt(self):
        return self._text()

    def _title(self):
        if 'title' in self.options.keys():
            return self.options['title']
        if 'title' in self._conf():
            return self._conf()['title']
        if 'text' in self.options.keys():
            return self.options['text']
        if 'text' in self._conf():
            return self._conf()['text']
        return ""

    def _logo(self):
        logo_fullpath = next((path for name, path in self._env().config['extref_logo_images'].items() \
                                                  if name == self._ref_type()), None)
        if not logo_fullpath:
            raise ValueError("There is no appropriate logo for the reference {0}".format(self._ref_type()))
        logo_path = logo_fullpath.replace(str(self._env().app.outdir), '')[1:]
        return self._image_tag(logo_path)

    def _image_tag(self, image_path):
        doc_dir = os.path.dirname(self.state.document.attributes['source'].replace(str(self._env().app.srcdir), ''))[1:]
        return '<img alt="{0}" title="{1}" src="{2}" style="{3}{4}"/>'.format(
            self._alt(), self._title(), os.path.relpath(image_path, doc_dir),
            self._width_tag_option(), self._height_tag_option()
        )

    def _width_tag_option(self):
        if 'width' in self.options.keys():
            width = self.options['width']
            if width[-1] in "0123456789":
                width += "px"
            return "width: {0};".format(width)
        return ""

    def _height_tag_option(self):
        if 'height' in self.options.keys():
            height = self.options['height']
            if height[-1] in "0123456789":
                height += "px"
            return "height: {0};".format(height)
        return ""

    def _text(self):
        if 'text' in self.options.keys():
            return self.options['text']
        if 'text' in self._conf():
            return self._conf()['text']
        return self._href()

def config_inited_handler(app, config):
    if config.extref_default_type and not config.extref_default_type in REF_TYPES:
        raise ValueError("Parameter extref_default_type should be in {0}".format(REF_TYPES))
    prepare_conf_json(app, config)
    prepare_images(app, config)

def prepare_conf_json(app, config):
    if not config.extref_conf:
        raise ValueError("Parameter extref_conf could not be empty")
    with open(os.path.join(app.srcdir, config.extref_conf)) as f:
        try:
            config.extref_conf = json.loads(f.read())
        except json.decoder.JSONDecodeError as e:
            msg = "Decode error in {0}. {1}".format(config.extref_conf, e.msg)
            raise json.decoder.JSONDecodeError(msg, e.doc, e.pos) from e

def prepare_images(app, config):
    extref_images_dir = os.path.join(app.outdir, config.extref_images_dir)
    if not os.path.isdir(extref_images_dir):
        os.makedirs(extref_images_dir)
    prepare_logo(app, config)

def prepare_logo(app, config):
    extref_logo_dir = os.path.join(app.outdir, config.extref_images_dir, LOGO_DIR)
    if not os.path.isdir(extref_logo_dir):
        os.makedirs(extref_logo_dir)
    if config.extref_logo_images:
        extref_logo_images = {}
        for logo_name, logo_src_path in config.extref_logo_images.items():
            logo_src_fullpath = os.path.join(app.srcdir, logo_src_path)
            logo_doc_fullpath = os.path.join(extref_logo_dir, "{0}{1}".format(logo_name, os.path.splitext(logo_src_path)[1]))
            extref_logo_images[logo_name] = logo_doc_fullpath
            if not os.path.isfile(logo_doc_fullpath):
                shutil.copy(logo_src_fullpath, logo_doc_fullpath)
        config.extref_logo_images = extref_logo_images

def ext_utils(util_name, conf_path, src_dir):
    globals()[util_name](conf_path, src_dir)

def check_using_notebook_names(conf_path, src_dir):
    """
    Calculate count of using for each notebook name from JSON configuration file.
    """
    import json

    with open(conf_path, 'r') as f:
        nb_names = json.load(f).keys()

    nb_counts = {nb_name: 0 for nb_name in nb_names}
    for root, dirs, files in os.walk(src_dir):
        for file in files:
            if os.path.splitext(file)[1] != ".rst":
                continue
            with open(os.path.join(root, file), 'r', errors='ignore') as f:
                data = f.read()
                for nb_name in nb_names:
                    nb_counts[nb_name] += data.count(" extref:: {0}".format(nb_name))

    for nb_name, count in sorted(nb_counts.items(), key=lambda p: p[1], reverse=True):
        print(nb_name, count)

def setup(app):
    app.add_config_value('extref_conf', None, 'html')
    app.add_config_value('extref_logo_images', None, 'html')
    app.add_config_value('extref_images_dir', IMAGES_DIR, 'html')
    app.add_config_value('extref_default_type', REF_TYPES[0], 'html')
    app.add_config_value('extref_default_ref', None, 'html')
    app.add_config_value('extref_default_image', None, 'html')
    app.add_config_value('extref_class', None, 'html')

    app.add_directive('extref', ExtRefDirective)

    app.connect('config-inited', config_inited_handler)

    return {
        'version': '0.3',
    }

if __name__ == '__main__':
    parser = ArgumentParser()
    parser.add_argument('-c', '--conf_path', required=True, metavar='CONF_PATH', help="Path to the JSON configuration file.")
    parser.add_argument('-s', '--src_dir', required=True, metavar='SRC_DIR', help="Path to the source directory.")
    parser.add_argument('-u', '--util_name', required=True, choices=AVAILABLE_UTILS, metavar='UTIL_NAME', help="Util name to use")
    args = parser.parse_args()

    ext_utils(args.util_name, args.conf_path, args.src_dir)