# -*- coding: utf-8 -*-
"""
    blockdiag.sphinx_ext
    ~~~~~~~~~~~~~~~~~~~~

    Allow blockdiag-formatted diagrams to be included in Sphinx-generated
    documents inline.

    :copyright: Copyright 2010 by Takeshi Komiya.
    :license: BSDL.
"""

import os
import re
import posixpath
import traceback
import pkg_resources
from collections import namedtuple
from docutils import nodes
from sphinx import addnodes
from sphinx.util import logging
from sphinx.util.osutil import ensuredir

import blockdiag.utils.rst.nodes
import blockdiag.utils.rst.directives
from blockdiag.utils.bootstrap import detectfont, Application
from blockdiag.utils.fontmap import FontMap
from blockdiag.utils.rst.directives import with_blockdiag

# fontconfig; it will be initialized on `builder-inited` event.
fontmap = None

logger = logging.getLogger(__name__)


class blockdiag_node(blockdiag.utils.rst.nodes.blockdiag):
    def to_drawer(self, image_format, builder, **kwargs):
        if 'filename' in kwargs:
            filename = kwargs.pop('filename')
        else:
            filename = self.get_abspath(image_format, builder)

        antialias = builder.config.blockdiag_antialias
        transparency = builder.config.blockdiag_transparency
        image = super(blockdiag_node, self).to_drawer(image_format, filename, fontmap,
                                                      antialias=antialias, transparency=transparency,
                                                      **kwargs)
        for node in image.diagram.traverse_nodes():
            node.href = resolve_reference(builder, node.href)

        return image

    def get_relpath(self, image_format, builder):
        options = dict(antialias=builder.config.blockdiag_antialias,
                       fontpath=builder.config.blockdiag_fontpath,
                       fontmap=builder.config.blockdiag_fontmap,
                       format=image_format,
                       transparency=builder.config.blockdiag_transparency)
        return posixpath.join(builder.imgpath, self.get_path(**options))

    def get_abspath(self, image_format, builder):
        options = dict(antialias=builder.config.blockdiag_antialias,
                       fontpath=builder.config.blockdiag_fontpath,
                       fontmap=builder.config.blockdiag_fontmap,
                       format=image_format,
                       transparency=builder.config.blockdiag_transparency)

        path = os.path.join(builder.outdir, builder.imagedir, self.get_path(**options))
        ensuredir(os.path.dirname(path))

        return path


class Blockdiag(blockdiag.utils.rst.directives.BlockdiagDirective):
    node_class = blockdiag_node

    def node2image(self, node, diagram):
        return node


def resolve_reference(builder, href):
    if href is None:
        return None

    pattern = re.compile("^:ref:`(.+?)`", re.UNICODE)
    matched = pattern.search(href)
    if matched is None:
        return href
    elif not hasattr(builder, 'current_docname'):  # ex. latex builder
        return matched.group(1)
    else:
        refid = matched.group(1)
        domain = builder.env.domains['std']
        node = addnodes.pending_xref(refexplicit=False)
        xref = domain.resolve_xref(builder.env, builder.current_docname, builder,
                                   'ref', refid, node, node)
        if xref:
            if 'refid' in xref:
                return "#" + xref['refid']
            else:
                return xref['refuri']
        else:
            logger.warning('undefined label: %s', refid)
            return None


def html_render_svg(self, node):
    image = node.to_drawer('SVG', self.builder, filename=None, nodoctype=True)
    image.draw()

    # align
    align = node['options'].get('align', 'default')
    self.body.append('<div class="align-%s">' % align)
    self.context.append('</div>\n')

    # reftarget
    for node_id in node['ids']:
        self.body.append('<span id="%s"></span>' % node_id)

    # resize image
    size = image.pagesize().resize(**node['options'])
    self.body.append(image.save(size))
    self.context.append('')


def html_render_clickablemap(self, image, width_ratio, height_ratio):
    href_nodes = [node for node in image.nodes if node.href]
    if not href_nodes:
        return

    self.body.append('<map name="map_%d">' % id(image))
    for node in href_nodes:
        x1, y1, x2, y2 = image.metrics.cell(node)

        x1 *= width_ratio
        x2 *= width_ratio
        y1 *= height_ratio
        y2 *= height_ratio
        areatag = '<area shape="rect" coords="%s,%s,%s,%s" href="%s">' % (x1, y1, x2, y2, node.href)
        self.body.append(areatag)

    self.body.append('</map>')


def html_render_png(self, node):
    image = node.to_drawer('PNG', self.builder)
    if not os.path.isfile(image.filename):
        image.draw()
        image.save()

    # align
    align = node['options'].get('align', 'default')
    self.body.append('<div class="align-%s">' % align)
    self.context.append('</div>\n')

    # link to original image
    relpath = node.get_relpath('PNG', self.builder)
    relpath = relpath.replace(os.path.sep, '/')
    if 'width' in node['options'] or 'height' in node['options'] or 'scale' in node['options']:
        self.body.append('<a class="reference internal image-reference" href="%s">' % relpath)
        self.context.append('</a>')
    else:
        self.context.append('')

    # <img> tag
    original_size = image.pagesize()
    resized = original_size.resize(**node['options'])
    img_attr = dict(src=relpath,
                    width=resized.width,
                    height=resized.height)

    if any(node.href for node in image.nodes):
        img_attr['usemap'] = "#map_%d" % id(image)

        width_ratio = float(resized.width) / original_size.width
        height_ratio = float(resized.height) / original_size.height
        html_render_clickablemap(self, image, width_ratio, height_ratio)

    if 'alt' in node['options']:
        img_attr['alt'] = node['options']['alt']

    self.body.append(self.starttag(node, 'img', '', empty=True, **img_attr))


@with_blockdiag
def html_visit_blockdiag(self, node):
    try:
        image_format = get_image_format_for(self.builder)
        if image_format.upper() == 'SVG':
            html_render_svg(self, node)
        else:
            html_render_png(self, node)
    except UnicodeEncodeError:
        if self.builder.config.blockdiag_debug:
            traceback.print_exc()

        msg = ("blockdiag error: UnicodeEncodeError caught "
               "(check your font settings)")
        logger.warning(msg)
        raise nodes.SkipNode
    except Exception as exc:
        if self.builder.config.blockdiag_debug:
            traceback.print_exc()

        logger.warning('dot code %r: %s', node['code'], exc)
        raise nodes.SkipNode


def html_depart_blockdiag(self, node):
    self.body.append(self.context.pop())
    self.body.append(self.context.pop())


def get_image_format_for(builder):
    if builder.format in ('html', 'slides'):
        image_format = builder.config.blockdiag_html_image_format.upper()
    elif builder.format == 'latex':
        if builder.config.blockdiag_tex_image_format:
            image_format = builder.config.blockdiag_tex_image_format.upper()
        else:
            image_format = builder.config.blockdiag_latex_image_format.upper()
    else:
        image_format = 'PNG'

    if image_format.upper() not in ('PNG', 'PDF', 'SVG'):
        raise ValueError('unknown format: %s' % image_format)

    if image_format.upper() == 'PDF':
        try:
            import reportlab  # NOQA: importing test
        except ImportError:
            raise ImportError('Could not output PDF format. Install reportlab.')

    return image_format


def on_builder_inited(self):
    # show deprecated message
    if self.builder.config.blockdiag_tex_image_format:
        logger.warning('blockdiag_tex_image_format is deprecated. Use blockdiag_latex_image_format.')

    # initialize fontmap
    global fontmap

    try:
        fontmappath = self.builder.config.blockdiag_fontmap
        fontmap = FontMap(fontmappath)
    except Exception:
        fontmap = FontMap(None)

    try:
        fontpath = self.builder.config.blockdiag_fontpath
        if isinstance(fontpath, str):
            fontpath = [fontpath]

        if fontpath:
            config = namedtuple('Config', 'font')(fontpath)
            fontpath = detectfont(config)
            fontmap.set_default_font(fontpath)
    except Exception:
        pass


def on_doctree_resolved(self, doctree, docname):
    if self.builder.format in ('html', 'slides'):
        return

    try:
        image_format = get_image_format_for(self.builder)
    except Exception as exc:
        if self.builder.config.blockdiag_debug:
            traceback.print_exc()

        logger.warning('blockdiag error: %s', exc)
        for node in doctree.traverse(blockdiag_node):
            node.parent.remove(node)

        return

    for node in doctree.traverse(blockdiag_node):
        try:
            with Application():
                relfn = node.get_relpath(image_format, self.builder)
                image = node.to_drawer(image_format, self.builder)
                if not os.path.isfile(image.filename):
                    image.draw()
                    image.save()

                image = nodes.image(uri=relfn, candidates={'*': relfn}, **node['options'])
                node.parent.replace(node, image)
        except Exception as exc:
            if self.builder.config.blockdiag_debug:
                traceback.print_exc()

            logger.warning('dot code %r: %s', node['code'], exc)
            node.parent.remove(node)


def setup(app):
    app.add_node(blockdiag_node,
                 html=(html_visit_blockdiag, html_depart_blockdiag))
    app.add_directive('blockdiag', Blockdiag)
    app.add_config_value('blockdiag_fontpath', None, 'html')
    app.add_config_value('blockdiag_fontmap', None, 'html')
    app.add_config_value('blockdiag_antialias', False, 'html')
    app.add_config_value('blockdiag_transparency', True, 'html')
    app.add_config_value('blockdiag_debug', False, 'html')
    app.add_config_value('blockdiag_html_image_format', 'PNG', 'html')
    app.add_config_value('blockdiag_tex_image_format', None, 'html')  # backward compatibility for 1.3.1
    app.add_config_value('blockdiag_latex_image_format', 'PNG', 'html')
    app.connect("builder-inited", on_builder_inited)
    app.connect("doctree-resolved", on_doctree_resolved)

    return {
        'version': pkg_resources.require('blockdiag')[0].version,
        'parallel_read_safe': True,
        'parallel_write_safe': True,
    }
