# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
This provides a sphinx extension able to render the support-matrix.ini
file into the developer documentation.

It is used via a single directive in the .rst file

  .. support_matrix::

"""
import configparser
from os import path
import re

from docutils import nodes
from docutils.parsers import rst
from sphinx.util.fileutil import copy_asset

KEY_PATTERN = re.compile("[^a-zA-Z0-9_]")
DRIVER_PREFIX = "driver."
FEATURE_PREFIX = 'operation.'
DRIVER_NOTES_PREFIX = "driver-notes."


class Matrix(object):
    """Represents the entire support matrix for project drivers"""

    def __init__(self, cfg):
        self.drivers = self._set_drivers(cfg)
        self.features = self._set_features(cfg)

    @staticmethod
    def _set_drivers(cfg):
        drivers = {}

        for section in cfg.sections():
            if not section.startswith(DRIVER_PREFIX):
                continue

            title = cfg.get(section, "title")

            link = None
            if cfg.has_option(section, 'link'):
                link = cfg.get(section, "link")

            driver = Driver(title, link)
            drivers[section] = driver

        return drivers

    def _set_features(self, cfg):
        features = []

        def _process_feature(section):
            if not cfg.has_option(section, "title"):
                raise Exception(
                    "'title' option missing in '[%s]' section" % section)

            title = cfg.get(section, "title")
            status = Feature.STATUS_OPTIONAL
            group = None

            if cfg.has_option(section, "status"):
                # The value is a string "status(group)" where
                # the 'group' part is optional
                status, group = re.match(r'^([^(]+)(?:\(([^)]+)\))?$',
                                         cfg.get(section, "status")).groups()

                if status not in Feature.STATUS_ALL:
                    raise ValueError(
                        "'status' option value '%s' in ['%s']"
                        "section must be one of (%s)" %
                        (status, section,
                         ", ".join(Feature.STATUS_ALL)))

            cli = []
            if cfg.has_option(section, "cli"):
                cli = cfg.get(section, "cli")

            api = None
            if cfg.has_option(section, "api"):
                api = cfg.get(section, "api")

            notes = None
            if cfg.has_option(section, "notes"):
                notes = cfg.get(section, "notes")
            return Feature(
                section, title,
                status=status, group=group, notes=notes, cli=cli, api=api)

        def _process_implementation(section, option, feature):
            if option not in self.drivers:
                raise Exception(
                    "'%s' section is not declared in the "
                    "INI file." % (option))

            status = cfg.get(section, option)
            if status not in Implementation.STATUS_ALL:
                raise ValueError(
                    "%s is set to %s in '[%s]' section but must be "
                    "one of (%s)" % (option, status, section, ", ".join(
                        Implementation.STATUS_ALL)))

            option_notes = ''.join([DRIVER_NOTES_PREFIX,
                                    option[len(DRIVER_PREFIX):]])
            notes = None
            if cfg.has_option(section, option_notes):
                notes = cfg.get(section, option_notes)

            impl = Implementation(status=status, notes=notes)
            feature.implementations[option] = impl

            return feature

        for section in cfg.sections():
            if not section.startswith(FEATURE_PREFIX):
                continue

            feature = _process_feature(section)

            # Now we've got the basic feature details, we must process
            # the backend driver implementation for each feature
            for option in cfg.options(section):
                if not option.startswith(DRIVER_PREFIX):
                    continue

                _process_implementation(section, option, feature)
            features.append(feature)

        return features


class Feature(object):
    STATUS_CHOICE = "choice"
    STATUS_CONDITION = "condition"
    STATUS_MANDATORY = "mandatory"
    STATUS_OPTIONAL = "optional"
    STATUS_MATURE = "mature"
    STATUS_IMMATURE = "immature"

    STATUS_ALL = [STATUS_MANDATORY, STATUS_OPTIONAL, STATUS_CHOICE,
                  STATUS_CONDITION, STATUS_MATURE, STATUS_IMMATURE]

    def __init__(self, key, title, status=STATUS_OPTIONAL,
                 group=None, notes=None, cli=(), api=None):
        self.key = key
        self.title = title
        self.status = status
        self.group = group
        self.notes = notes
        self.cli = cli
        self.api = api

        self.implementations = {}


class Implementation(object):
    STATUS_COMPLETE = "complete"
    STATUS_PARTIAL = "partial"
    STATUS_MISSING = "missing"
    STATUS_UNKNOWN = "unknown"

    STATUS_ALL = [STATUS_COMPLETE, STATUS_MISSING,
                  STATUS_PARTIAL, STATUS_UNKNOWN]

    def __init__(self, status=STATUS_MISSING, notes=None):
        self.status = status
        self.notes = notes


STATUS_SYMBOLS = {
    Implementation.STATUS_COMPLETE: u"\u2714",
    Implementation.STATUS_MISSING: u"\u2716",
    Implementation.STATUS_PARTIAL: u"\u2714",
    Implementation.STATUS_UNKNOWN: u"?"
}


class Driver(object):
    def __init__(self, title, link=None):
        """Driver object.

        :param title: Human readable name for plugin
        :param link: A URL to documentation about the driver.
        """
        self.title = title
        self.link = link


class Directive(rst.Directive):

    # support-matrix.ini is the arg
    required_arguments = 1

    def run(self):
        matrix = self._load_support_matrix()
        return self._build_markup(matrix)

    def _load_support_matrix(self):
        """Parse support-matrix.ini file.

        Reads the support-matrix.ini file and populates an instance of the
        Matrix class with all the data.

        :returns: Matrix instance
        """

        cfg = configparser.ConfigParser()
        env = self.state.document.settings.env
        fname = self.arguments[0]
        rel_fpath, fpath = env.relfn2path(fname)

        with open(fpath) as fp:
            cfg.read_file(fp)

        # This ensures that the docs are rebuilt whenever the
        # .ini file changes
        env.note_dependency(rel_fpath)

        matrix = Matrix(cfg)
        return matrix

    def _build_markup(self, matrix):
        """Constructs the docutils content for the support matrix."""
        content = []
        self._build_summary(matrix, content)
        self._build_details(matrix, content)
        self._build_notes(content)
        return content

    @staticmethod
    def _build_summary(matrix, content):
        """Constructs the content for the summary of the support matrix.

        The summary consists of a giant table, with one row
        for each feature, and a column for each backend
        driver. It provides an 'at a glance' summary of the
        status of each driver.
        """

        summary_title = nodes.subtitle(text="Summary")
        summary = nodes.table(classes=["sp_feature_cells"])
        cols = len(matrix.drivers.keys())

        # Add two columns for the Feature and Status columns.
        cols += 2

        summary_group = nodes.tgroup(cols=cols)
        summary_body = nodes.tbody()
        summary_head = nodes.thead()

        for i in range(cols):
            summary_group.append(nodes.colspec(colwidth=1))
        summary_group.append(summary_head)
        summary_group.append(summary_body)
        summary.append(summary_group)
        content.append(summary_title)
        content.append(summary)

        # This sets up all the column headers - two fixed
        # columns for feature name & status
        header = nodes.row()
        blank = nodes.entry(classes=["sp_feature_cells"])
        blank.append(nodes.emphasis(text="Feature"))
        header.append(blank)
        blank = nodes.entry(classes=["sp_feature_cells"])
        blank.append(nodes.emphasis(text="Status"))
        header.append(blank)
        summary_head.append(header)

        # then one column for each backend driver
        impls = sorted(matrix.drivers,
                       key=lambda x: matrix.drivers[x].title)
        for key in impls:
            driver = matrix.drivers[key]
            implcol = nodes.entry(classes=["sp_feature_cells"])
            header.append(implcol)
            if driver.link:
                uri = driver.link
                target_ref = nodes.reference("", refuri=uri)
                target_txt = nodes.inline()
                implcol.append(target_txt)
                target_txt.append(target_ref)
                target_ref.append(nodes.strong(text=driver.title))
            else:
                implcol.append(nodes.strong(text=driver.title))

        # We now produce the body of the table, one row for
        # each feature to report on
        for feature in matrix.features:
            item = nodes.row()

            # the hyperlink driver name linking to details
            feature_id = re.sub(KEY_PATTERN, "_", feature.key)

            # first the fixed columns for title/status
            key_col = nodes.entry(classes=["sp_feature_cells"])
            item.append(key_col)
            key_ref = nodes.reference(refid=feature_id)
            key_txt = nodes.inline()
            key_col.append(key_txt)
            key_txt.append(key_ref)
            key_ref.append(nodes.strong(text=feature.title))

            status_col = nodes.entry(classes=["sp_feature_cells"])
            item.append(status_col)
            status_col.append(nodes.inline(
                text=feature.status,
                classes=["sp_feature_" + feature.status]))

            # and then one column for each backend driver
            for key in impls:
                impl = feature.implementations[key]
                impl_col = nodes.entry(classes=["sp_feature_cells"])
                item.append(impl_col)

                key_id = re.sub(KEY_PATTERN, "_",
                                "{}_{}".format(feature.key, key))

                impl_ref = nodes.reference(refid=key_id)
                impl_txt = nodes.inline()
                impl_col.append(impl_txt)
                impl_txt.append(impl_ref)

                status = STATUS_SYMBOLS.get(impl.status, "")

                impl_ref.append(nodes.literal(
                    text=status,
                    classes=["sp_impl_summary", "sp_impl_" + impl.status]))

            summary_body.append(item)

    def _build_details(self, matrix, content):
        """Constructs the content for the details of the support matrix."""

        details_title = nodes.subtitle(text="Details")
        details = nodes.bullet_list()

        content.append(details_title)
        content.append(details)

        # One list entry for each feature we're reporting on
        for feature in matrix.features:
            item = nodes.list_item()

            status = feature.status
            if feature.group is not None:
                status += "({})".format(feature.group)

            feature_id = re.sub(KEY_PATTERN, "_", feature.key)

            # Highlight the feature title name
            item.append(nodes.strong(text=feature.title, ids=[feature_id]))

            # Add maturity status
            para = nodes.paragraph()
            para.append(nodes.strong(text="Status: {}. ".format(status)))
            item.append(para)

            if feature.api is not None:
                para = nodes.paragraph()
                para.append(
                    nodes.strong(text="API Alias: {} ".format(feature.api)))
                item.append(para)

            if feature.cli:
                item.append(self._create_cli_paragraph(feature))

            if feature.notes is not None:
                item.append(self._create_notes_paragraph(feature.notes))

            para_divers = nodes.paragraph()
            para_divers.append(nodes.strong(text="Driver Support:"))
            # A sub-list giving details of each backend driver
            impls = nodes.bullet_list()
            keys = sorted(feature.implementations,
                          key=lambda x: matrix.drivers[x].title)
            for key in keys:
                driver = matrix.drivers[key]
                impl = feature.implementations[key]
                subitem = nodes.list_item()

                key_id = re.sub(KEY_PATTERN, "_",
                                "{}_{}".format(feature.key, key))

                subitem += [
                    nodes.strong(text="{}: ".format(driver.title)),
                    nodes.literal(text=impl.status,
                                  classes=["sp_impl_{}".format(impl.status)],
                                  ids=[key_id]),
                ]

                if impl.notes is not None:
                    subitem.append(self._create_notes_paragraph(impl.notes))

                impls.append(subitem)

            para_divers.append(impls)
            item.append(para_divers)
            details.append(item)

    @staticmethod
    def _build_notes(content):
        """Constructs a list of notes content for the support matrix.

        This is generated as a bullet list.
        """
        notes_title = nodes.subtitle(text="Notes:")
        notes = nodes.bullet_list()

        content.append(notes_title)
        content.append(notes)

        for note in ["This document is a continuous work in progress"]:
            item = nodes.list_item()
            item.append(nodes.strong(text=note))
            notes.append(item)

    @staticmethod
    def _create_cli_paragraph(feature):
        """Create a paragraph which represents the CLI commands of the feature

        The paragraph will have a bullet list of CLI commands.
        """
        para = nodes.paragraph()
        para.append(nodes.strong(text="CLI commands:"))
        commands = nodes.bullet_list()
        for c in feature.cli.split(";"):
            cli_command = nodes.list_item()
            cli_command += nodes.literal(text=c, classes=["sp_cli"])
            commands.append(cli_command)
        para.append(commands)
        return para

    @staticmethod
    def _create_notes_paragraph(notes):
        """Constructs a paragraph which represents the implementation notes

        The paragraph consists of text and clickable URL nodes if links were
        given in the notes.
        """
        para = nodes.paragraph()
        para.append(nodes.strong(text="Notes: "))
        # links could start with http:// or https://
        link_idxs = [m.start() for m in re.finditer('https?://', notes)]
        start_idx = 0
        for link_idx in link_idxs:
            # assume the notes start with text (could be empty)
            para.append(nodes.inline(text=notes[start_idx:link_idx]))
            # create a URL node until the next text or the end of the notes
            link_end_idx = notes.find(" ", link_idx)
            if link_end_idx == -1:
                # In case the notes end with a link without a blank
                link_end_idx = len(notes)
            uri = notes[link_idx:link_end_idx + 1]
            para.append(nodes.reference("", uri, refuri=uri))
            start_idx = link_end_idx + 1

        # get all text after the last link (could be empty) or all of the
        # text if no link was given
        para.append(nodes.inline(text=notes[start_idx:]))
        return para


def on_build_finished(app, exc):
    if exc is None:
        src = path.join(path.abspath(path.dirname(__file__)),
                        'support-matrix.css')
        dst = path.join(app.outdir, '_static')
        copy_asset(src, dst)


def setup(app):
    app.add_directive('support_matrix', Directive)
    app.add_css_file('support-matrix.css')
    app.connect('build-finished', on_build_finished)
    return {
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
