# -*- coding: utf-8 -*-
"""Simple Markdown writer for reStructuredText.

See:

* https://daringfireball.net/projects/markdown/syntax
* https://guides.github.com/pdfs/markdown-cheatsheet-online.pdf

**********************
Copyright and Licenses
**********************

nb2plots
========

The nb2plots package, including all examples, code snippets and attached
documentation is covered by the 2-clause BSD license.

    Copyright (c) 2015-2018, Matthew Brett
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions are
    met:

    1. Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
    THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
    PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
    CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
    EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
    PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
    PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
    LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
    NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
    SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

3rd party code and data
=======================

Some code distributed within the nb2plots sources was developed by other
projects. This code is distributed under their respective licenses that are
listed below.

Matplotlib plot extension
-------------------------

The ``nbplots`` module is an edited version of
``matplotlib/lib/matplotlib/sphinxext/plot_directive.py``.

Among the major changes are the following:

* rename of 'plot' directive to 'nbplot';
* rename of 'plot_*' configuration options to 'nbplot_*' equivalent;
* nbplot removes the option to point to Python script file rather than
  using the code in the directive;
* nbplot keeps the namespace across directives in the same page, by default;
* include-source is True by default;
* nbplot can include multiple "parts" that can be selected via the
  "render-parts" and "run-parts" options;
* do not use cached output when rebuilding page to avoid errors from stale
  output;
* fix bug including empty parens when ``nofigs`` not specified (also fixed
  upstream);
* empty ``setup_module`` to avoid breakage of nose doctests;
* fix of plot doctest.

The following license covers the plot directive code:

    LICENSE AGREEMENT FOR MATPLOTLIB 1.2.0
    --------------------------------------

    1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the
    Individual or Organization ("Licensee") accessing and otherwise using
    matplotlib software in source or binary form and its associated
    documentation.

    2. Subject to the terms and conditions of this License Agreement, JDH
    hereby grants Licensee a nonexclusive, royalty-free, world-wide license
    to reproduce, analyze, test, perform and/or display publicly, prepare
    derivative works, distribute, and otherwise use matplotlib 1.2.0
    alone or in any derivative version, provided, however, that JDH's
    License Agreement and JDH's notice of copyright, i.e., "Copyright (c)
    2002-2011 John D. Hunter; All Rights Reserved" are retained in
    matplotlib 1.2.0 alone or in any derivative version prepared by
    Licensee.

    3. In the event Licensee prepares a derivative work that is based on or
    incorporates matplotlib 1.2.0 or any part thereof, and wants to
    make the derivative work available to others as provided herein, then
    Licensee hereby agrees to include in any such work a brief summary of
    the changes made to matplotlib 1.2.0.

    4. JDH is making matplotlib 1.2.0 available to Licensee on an "AS
    IS" basis.  JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
    IMPLIED.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND
    DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
    FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB 1.2.0
    WILL NOT INFRINGE ANY THIRD PARTY RIGHTS.

    5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB
    1.2.0 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR
    LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING
    MATPLOTLIB 1.2.0, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF
    THE POSSIBILITY THEREOF.

    6. This License Agreement will automatically terminate upon a material
    breach of its terms and conditions.

    7. Nothing in this License Agreement shall be deemed to create any
    relationship of agency, partnership, or joint venture between JDH and
    Licensee.  This License Agreement does not grant permission to use JDH
    trademarks or trade name in a trademark sense to endorse or promote
    products or services of Licensee, or any third party.

    8. By copying, installing or otherwise using matplotlib 1.2.0,
    Licensee agrees to be bound by the terms and conditions of this License
    Agreement.

rst2md
------

The files nb2plots/doctree2md.py and scripts/rst2md derive from the
"markdown.py" and "rst2md.py" files from "rst2md" project by Chris Wrench:
https://github.com/cgwrench/rst2md

``rst2md`` has the following license:

     Copyright (c) 2012, C. G. Wrench <c.g.wrench@gmail.com>
     All rights reserved.

     Redistribution and use in source and binary forms, with or without
     modification, are permitted provided that the following conditions are
     met:

     1. Redistributions of source code must retain the above copyright notice,
        this list of conditions and the following disclaimer.
     2. Redistributions in binary form must reproduce the above copyright
        notice, this list of conditions and the following disclaimer in the
        documentation and/or other materials provided with the distribution.

     THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

from __future__ import unicode_literals

import re
from textwrap import dedent
import posixpath

__docformat__ = 'reStructuredText'

from docutils import frontend, nodes, writers, languages
from collections import OrderedDict

class IndentLevel(object):
    """Class to hold text being written for a certain indentation level.

    For example, all text in list_elements need to be indented.  A list_element
    creates one of these indentation levels, and all text contained in the
    list_element gets written to this IndentLevel.  When we leave the
    list_element, we ``write`` the text with suitable prefixes to the next
    level down, which might be the base of the document (document body) or
    another indentation level, if this is - for example - a nested list.

    In most respects, IndentLevel behaves like a list.
    """
    def __init__(self, base, prefix, first_prefix=None):
        self.base = base  # The list to which we eventually write
        self.prefix = prefix  # Text prepended to lines
        # Text prepended to first list
        self.first_prefix = prefix if first_prefix is None else first_prefix
        # Our own list to which we append before doing a ``write``
        self.content = []

    def append(self, new):
        self.content.append(new)

    def __getitem__(self, index):
        return self.content[index]

    def __len__(self):
        return len(self.content)

    def __bool__(self):
        return len(self) != 0

    def write(self):
        """Add ``self.contents`` with current ``prefix`` and ``first_prefix``

        Add processed ``self.contents`` to ``self.base``.  The first line has
        ``first_prefix`` prepended, further lines have ``prefix`` prepended.

        Empty (all whitespace) lines get written as bare carriage returns, to
        avoid ugly extra whitespace.
        """
        string = ''.join(self.content)
        lines = string.splitlines(True)
        if len(lines) == 0:
            return
        texts = [self.first_prefix + lines[0]]
        for line in lines[1:]:
            if line.strip() == '':  # avoid prefix for empty lines
                texts.append('\n')
            else:
                texts.append(self.prefix + line)
        self.base.append(''.join(texts))

def _make_method(to_add):
    """Make a method that adds `to_add`

    We need this function so that `to_add` is a fresh and unique
    variable at the time the method is defined.
    """
    def method(self, node):
        self.add(to_add)

    return method

def add_pref_suff(pref_suff_map):
    """Decorator adds visit, depart methods for prefix/suffix pairs."""
    def dec(cls):
        # Need _make_method to ensure new variable picked up for each iteration
        # of the loop.  The defined method picks up this new variable in its
        # scope.
        for key, (prefix, suffix) in pref_suff_map.items():
            setattr(cls, 'visit_' + key, _make_method(prefix))
            setattr(cls, 'depart_' + key, _make_method(suffix))
        return cls

    return dec

def add_pass_thru(pass_thrus):
    """Decorator adds explicit pass-through visit and depart methods."""
    def meth(self, node):
        pass

    def dec(cls):
        for element_name in pass_thrus:
            for meth_prefix in ('visit_', 'depart_'):
                meth_name = meth_prefix + element_name
                if hasattr(cls, meth_name):
                    raise ValueError(
                        'method name {} already defined'.format(meth_name)
                    )
                setattr(cls, meth_name, meth)
        return cls

    return dec

# Characters that should be escaped in Markdown
ESCAPE_RE = re.compile(r'([\\*`])')

# Doctree elements for which Markdown element is <prefix><content><suffix>
PREF_SUFF_ELEMENTS = {
    'emphasis': ('*', '*'),  # Could also use ('_', '_')
    'strong': ('**', '**'),  # Could also use ('__', '__')
    'literal': ('`', '`'),
    'subscript': ('<sub>', '</sub>'),
    'superscript': ('<sup>', '</sup>'),
}

# Doctree elements explicitly passed through without extra markup
PASS_THRU_ELEMENTS = (
    'document',
    'container',
    'target',
    'inline',
    'definition_list',
    'definition_list_item',
    'term',
    'field_list',
    'field_list_item',
    'field',
    'field_name',
    'mpl_hint',
    'pending_xref',
    'compound',
    'line',
    'line_block',
)

@add_pass_thru(PASS_THRU_ELEMENTS)
@add_pref_suff(PREF_SUFF_ELEMENTS)
class Translator(nodes.NodeVisitor):

    std_indent = '    '

    def __init__(self, document, builder=None):
        nodes.NodeVisitor.__init__(self, document)
        self.builder = builder
        self.settings = settings = document.settings
        lcode = settings.language_code
        self.language = languages.get_language(lcode, document.reporter)
        # Not-None here indicates Markdown should use HTTP for internal and
        # download links.
        self.markdown_http_base = (
            builder.markdown_http_base if builder else None
        )
        # Warn only once per writer about unsupported elements
        self._warned = set()
        # Lookup table to get section list from name
        self._lists = OrderedDict((('head', []), ('body', []), ('foot', [])))
        # Reset attributes modified by reading
        self.reset()
        # Attribute shortcuts
        self.head, self.body, self.foot = self._lists.values()

    def reset(self):
        """Initialize object for fresh read."""
        for part in self._lists.values():
            part[:] = []

        # Current section heading level during writing
        self.section_level = 0

        # FIFO list of list prefixes, while writing nested lists.  Each element
        # corresponds to one level of nesting.  Thus ['1. ', '1. ', '* '] would
        # occur when writing items of an unordered list, that is nested within
        # an ordered list, that in turn is nested in another ordered list.
        self.list_prefixes = []

        # FIFO list of indentation levels.  When we are writing a block of text
        # that should be indented, we create a new indentation level.  We only
        # write the text when we leave the indentation level, so we can insert
        # the correct prefix for every line.
        self.indent_levels = []

        # Flag indicating we are processing docinfo items
        self._in_docinfo = False

        # Flag for whether to escape characters
        self._escape_text = True

    def astext(self):
        """Return the final formatted document as a string."""
        parts = [''.join(lines).strip() for lines in self._lists.values()]
        parts = [part + '\n\n' for part in parts if part]
        return ''.join(parts).strip() + '\n'

    def ensure_eol(self):
        """Ensure the last line in current base is terminated by new line."""
        out = self.get_current_output()
        if out and out[-1] and out[-1][-1] != '\n':
            out.append('\n')

    def get_current_output(self, section='body'):
        """Get list or IndentLevel to which we are currently writing."""
        return (
            self.indent_levels[-1]
            if self.indent_levels else self._lists[section]
        )

    def add(self, string, section='body'):
        """Add `string` to `section` or current output.

        Parameters
        ----------
        string : str
            String to add to output document
        section : {'body', 'head', 'foot'}, optional
            Section of document that generated text should be appended to, if
            not already appending to an indent level.  If already appending to
            an indent level, method will ignore `section`.  Use
            :meth:`add_section` to append to a section unconditionally.
        """
        self.get_current_output(section).append(string)

    def add_section(self, string, section='body'):
        """Add `string` to `section` regardless of current output.

        Can be useful when forcing write to header or footer.

        Parameters
        ----------
        string : str
            String to add to output document
        section : {'body', 'head', 'foot'}, optional
            Section of document that generated text should be appended to.
        """
        self._lists[section].append(string)

    def start_level(self, prefix, first_prefix=None, section='body'):
        """Create a new IndentLevel with `prefix` and `first_prefix`"""
        base = (
            self.indent_levels[-1].content
            if self.indent_levels else self._lists[section]
        )
        level = IndentLevel(base, prefix, first_prefix)
        self.indent_levels.append(level)

    def finish_level(self):
        """Remove most recent IndentLevel and write contents."""
        level = self.indent_levels.pop()
        level.write()

    def escape_chars(self, txt):
        # Escape (some) characters with special meaning for Markdown
        return ESCAPE_RE.sub(r'\\\1', txt)

    def visit_Text(self, node):
        text = node.astext().replace('\r\n', '\n')
        if self._escape_text:
            text = self.escape_chars(text)
        self.add(text)

    def depart_Text(self, node):
        pass

    def visit_comment(self, node):
        txt = node.astext()
        if txt.strip():
            self.add('<!-- ' + node.astext() + ' -->\n')
        raise nodes.SkipNode

    def visit_docinfo(self, node):
        self._in_docinfo = True

    def depart_docinfo(self, node):
        self._in_docinfo = False

    def process_docinfo_item(self, node):
        """Called explicitly from methods in this class."""
        self.add_section('% {}\n'.format(node.astext()), section='head')
        raise nodes.SkipNode

    def visit_definition(self, node):
        self.add('\n\n')
        self.start_level('    ')

    def depart_definition(self, node):
        self.finish_level()

    visit_field_body = visit_definition

    depart_field_body = depart_definition

    def visit_paragraph(self, node):
        pass

    def depart_paragraph(self, node):
        self.ensure_eol()
        self.add('\n')

    def visit_math_block(self, node):
        # docutils math block
        self._escape_text = False
        self.add('$$\n')

    def depart_math_block(self, node):
        self._escape_text = True
        self.ensure_eol()
        self.add('$$\n\n')

    def visit_displaymath(self, node):
        # sphinx math blocks become displaymath
        self.add('$$\n{}\n$$\n\n'.format(node['latex']))
        raise nodes.SkipNode

    def visit_math(self, node):
        # sphinx math node has 'latex' attribute, docutils does not
        if 'latex' in node:  # sphinx math node
            self.add('${}$'.format(node['latex']))
            raise nodes.SkipNode
        # docutils math node
        self._escape_text = False
        self.add('$')

    def depart_math(self, node):
        # sphinx node skipped in visit, only docutils gets here
        self._escape_text = True
        self.add('$')

    def visit_literal_block(self, node):
        self._escape_text = False
        code_type = node['classes'][1] if 'code' in node['classes'] else ''
        if 'language' in node:
            code_type = node['language']
        self.add('```' + code_type + '\n')

    def depart_literal_block(self, node):
        self._escape_text = True
        self.ensure_eol()
        self.add('```\n\n')

    def visit_doctest_block(self, node):
        self._escape_text = False
        self.add('```python\n')

    depart_doctest_block = depart_literal_block

    def visit_block_quote(self, node):
        self.start_level('> ')

    def depart_block_quote(self, node):
        self.finish_level()

    def visit_section(self, node):
        self.section_level += 1

    def depart_section(self, node):
        self.section_level -= 1

    def visit_enumerated_list(self, node):
        self.list_prefixes.append('1. ')

    def depart_enumerated_list(self, node):
        self.list_prefixes.pop()

    def visit_bullet_list(self, node):
        self.list_prefixes.append('* ')

    depart_bullet_list = depart_enumerated_list

    def visit_list_item(self, node):
        first_prefix = self.list_prefixes[-1]
        prefix = ' ' * len(first_prefix)
        self.start_level(prefix, first_prefix)

    def depart_list_item(self, node):
        self.finish_level()

    def visit_problematic(self, node):
        self.add('\n\n```\n{}\n```\n\n'.format(node.astext()))
        raise nodes.SkipNode

    def visit_system_message(self, node):
        if node['level'] < self.document.reporter.report_level:
            # Level is too low to display
            raise nodes.SkipNode
        line = ', line %s' % node['line'] if node.hasattr('line') else ''
        self.add(
            '```\nSystem Message: {}:{}\n\n{}\n```\n\n'.format(
                node['source'], line, node.astext()
            )
        )
        raise nodes.SkipNode

    def visit_title(self, node):
        self.add((self.section_level + 1) * '#' + ' ')

    def depart_title(self, node):
        self.ensure_eol()
        self.add('\n')

    def visit_subtitle(self, node):
        self.add((self.section_level + 2) * '#' + ' ')

    depart_subtitle = depart_title

    def visit_transition(self, node):
        # Simply replace a transition by a horizontal rule.
        # Could use three or more '*', '_' or '-'.
        self.add('\n---\n\n')
        raise nodes.SkipNode

    def _refuri2http(self, node):
        # Replace 'refuri' in reference with HTTP address, if possible
        # None for no possible address
        url = node.get('refuri')
        if not node.get('internal'):
            return url
        # If HTTP page build URL known, make link relative to that.
        if not self.markdown_http_base:
            return node.get("refuri")
        this_doc = self.builder.current_docname
        if url in (None, ''):  # Reference to this doc
            url = self.builder.get_target_uri(this_doc)
        else:  # URL is relative to the current docname.
            this_dir = posixpath.dirname(this_doc)
            if this_dir:
                url = posixpath.normpath('{}/{}'.format(this_dir, url))
        url = '{}/{}'.format(self.markdown_http_base, url)
        if 'refid' in node:
            url += '#' + node['refid']
        return url

    def visit_reference(self, node):
        # If no target possible, pass through.
        url = self._refuri2http(node)
        if url is None:
            return
        self.add('[')
        for child in node.children:
            child.walkabout(self)
        self.add(']({})'.format(url))
        raise nodes.SkipNode

    def depart_reference(self, node):
        pass

    def visit_nbplot_epilogue(self, node):
        raise nodes.SkipNode

    def visit_nbplot_not_rendered(self, node):
        raise nodes.SkipNode

    def visit_code_links(self, node):
        raise nodes.SkipNode

    def visit_index(self, node):
        # Drop index entries
        raise nodes.SkipNode

    def visit_substitution_definition(self, node):
        # Drop substitution definitions - the doctree already contains the text
        # with substitutions applied.
        raise nodes.SkipNode

    def visit_only(self, node):
        if node['expr'] == 'markdown':
            self.add(dedent(node.astext()) + '\n')
        raise nodes.SkipNode

    def visit_runrole_reference(self, node):
        raise nodes.SkipNode

    def visit_download_reference(self, node):
        # If not resolving internal links, or there is no filename specified,
        # pass through.
        filename = node.get('filename')
        if None in (self.markdown_http_base, filename):
            return
        target_url = '{}/_downloads/{}'.format(
            self.markdown_http_base, filename
        )
        self.add('[{}]({})'.format(node.astext(), target_url))
        raise nodes.SkipNode

    def depart_download_reference(self, node):
        pass

    visit_compact_paragraph = visit_paragraph

    depart_compact_paragraph = depart_paragraph

    def visit_nbplot_container(self, node):
        pass

    def depart_nbplot_container(self, node):
        pass

    def unknown_visit(self, node):
        """Warn once per instance for unsupported nodes.

        Intercept docinfo items if in docinfo block.
        """
        if self._in_docinfo:
            self.process_docinfo_item(node)
            return
        # We really don't know this node type, warn once per node type
        node_type = node.__class__.__name__
        if node_type not in self._warned:
            self.document.reporter.warning(
                'The ' + node_type + ' element not yet supported in Markdown.'
            )
            self._warned.add(node_type)
        raise nodes.SkipNode

class Writer(writers.Writer):

    supported = ('markdown', )
    """Formats this writer supports."""

    output = None
    """Final translated form of `document`."""

    # Add configuration settings for additional Markdown flavours here.
    settings_spec = (
        'Markdown-Specific Options', None, (
            (
                'Extended Markdown syntax.', ['--extended-markdown'], {
                    'default': 0,
                    'action': 'store_true',
                    'validator': frontend.validate_boolean
                }
            ),
            (
                'Strict Markdown syntax. Default: true', ['--strict-markdown'],
                {
                    'default': 1,
                    'action': 'store_true',
                    'validator': frontend.validate_boolean
                }
            ),
        )
    )

    translator_class = Translator

    def __init__(self, builder=None):
        writers.Writer.__init__(self)
        self.builder = builder

    def translate(self):
        visitor = self.builder.create_translator(self.document, self.builder)
        self.document.walkabout(visitor)
        self.output = visitor.astext()
