# -*- coding: utf-8 -*-

import os

import gzip
import logging
import xml.dom

try:
    import Image
    import ImageDraw
    import ImageFont
except ImportError:
    try:
        from PIL import Image, ImageDraw, ImageFont  # lint:ok
    except ImportError:
        __logger = logging.getLogger(__package__)
        __logger.info('PIL not found. Image output disabled.')
        Image = ImageDraw = ImageFont = None  # lint:ok

from abc import ABCMeta, abstractmethod

from steenzout.object import Object
from steenzout.barcode import metadata


SIZE = '{0:.3f}mm'
COMMENT = 'Autogenerated with %s %s' % (__package__, metadata.__release__)
PATH = os.path.dirname(os.path.abspath(__file__))
FONT = os.path.join(PATH, 'fonts/DejaVuSansMono.ttf')


def mm2px(value, dpi=300):
    """Converts the given value in millimeters into dots per inch.

    Args:
        value(float): value, in millimeters.
        dpi (int): resolution, in dots per inch.

    Returns:
        (float): value, in dots per inch.
    """
    return (value * dpi) / 25.4


def pt2mm(value):
    """Converts given value in points to millimeters.

    Args:
        value (int): value in points.

    Returns:
        (float): value, in millimeters.
    """
    return value * 0.352777778


def _set_attributes(element, **attributes):
    for key, value in attributes.items():
        element.setAttribute(key, value)


def create_svg_object():
    """Returns a blank SVG document.

    Returns:
        (:py:class:`xml.dom.minidom.DocumentType`): XML document.
    """
    imp = xml.dom.getDOMImplementation()
    doctype = imp.createDocumentType(
        'svg',
        '-//W3C//DTD SVG 1.1//EN',
        'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'
    )
    document = imp.createDocument(None, 'svg', doctype)
    _set_attributes(document.documentElement, version='1.1', xmlns='http://www.w3.org/2000/svg')
    return document


class Interface:
    """Writer interface."""
    __metaclass__ = ABCMeta

    @classmethod
    def __subclasshook__(cls, clazz):
        if cls is Interface:
            if any('save' in C.__dict__ for C in clazz.__mro__):
                return True
        return NotImplemented

    @abstractmethod
    def save(self, filename, content):
        """Saves contents to `filename`.

        Args:
            filename (str): filename without extension.
            content (str): output of rendering process.

        Returns:
            (str): the filename.
        """
        return None


class Base(Object, Interface):
    """Base class for all writers.

    Initializes the basic writer options.
    Sub-classes can add more attributes and
    can set them directly or using `self.set_options(option=value)`.

    Args:
        initialize (function):
            Callback for initializing the inheriting writer.
            Is called: `callback_initialize(raw_code)`
        paint_module (function):
            Callback for painting one barcode module.
            Is called: `callback_paint_module(xpos, ypos, width, color)`
        paint_text (function):
            Callback for painting the text under the barcode.
            Is called: `callback_paint_text(xpos, ypos)` using `self.text`
            as text.
        finish (function):
            Callback for doing something with the completely rendered
            output.
            Is called: `return callback_finish()` and must return the
            rendered output.
    """

    def __init__(self, initialize=None, paint_module=None, paint_text=None,
                 finish=None):
        self._callbacks = dict(
            initialize=initialize,
            paint_module=paint_module,
            paint_text=paint_text,
            finish=finish
        )

        self.module_width = 10
        self.module_height = 10
        self.font_size = 10
        self.quiet_zone = 6.5
        self.background = 'white'
        self.foreground = 'black'
        self.text = ''
        self.text_distance = 5
        self.center_text = True

    def calculate_size(self, modules_per_line, number_of_lines, dpi=300):
        """Calculates the size of the barcode in pixel.

        Args:
            modules_per_line (int): number of modules in one line.
            number_of_lines (int):  number of lines of the barcode.
            dpi (int): DPI to calculate.

        Returns:
            (tuple[int, int]): Width and height of the barcode in pixel.
        """
        width = 2 * self.quiet_zone + modules_per_line * self.module_width
        height = 2.0 + self.module_height * number_of_lines
        if self.font_size and self.text:
            height += pt2mm(self.font_size) / 2 + self.text_distance
        return int(mm2px(width, dpi)), int(mm2px(height, dpi))

    @abstractmethod
    def save(self, filename, output):
        """See :py:func:`Interface.save`."""

    def register_callback(self, action, callback):
        """Register one of the three callbacks if not given at instance
        creation.

        Args:
            action (str):
                One of 'initialize', 'paint_module', 'paint_text', 'finish'.
            callback (function):
                The callback function for the given action.
        """
        self._callbacks[action] = callback

    def set_options(self, options):
        """Sets the given options as instance attributes (only if they are known).

        Args:
            options (dict):
                All known instance attributes and more if the child class
                has defined them before this call.
        """
        for key, val in options.items():
            key = key.lstrip('_')
            if hasattr(self, key):
                setattr(self, key, val)

    def render(self, code):
        """Renders the barcode to whatever the inheriting writer provides,
        using the registered callbacks.

        Args:
            code (list[str]): list of strings matching the writer spec (only contain 0 or 1).
        """
        if self._callbacks['initialize'] is not None:
            self._callbacks['initialize'](code)
        ypos = 1.0
        for line in code:
            # Left quiet zone is x startposition
            xpos = self.quiet_zone
            for mod in line:
                if mod == '0':
                    color = self.background
                else:
                    color = self.foreground
                self._callbacks['paint_module'](xpos, ypos, self.module_width,
                                                color)
                xpos += self.module_width
            # Add right quiet zone to every line
            self._callbacks['paint_module'](xpos, ypos, self.quiet_zone,
                                            self.background)
            ypos += self.module_height
        if self.text and self._callbacks['paint_text'] is not None:
            ypos += self.text_distance
            if self.center_text:
                xpos = xpos / 2.0
            else:
                xpos = self.quiet_zone + 4.0
            self._callbacks['paint_text'](xpos, ypos)
        return self._callbacks['finish']()


class SVG(Base):

    def __init__(self):
        Base.__init__(self, self._init, self._create_module,
                      self._create_text, self._finish)
        self.compress = False
        self.dpi = 25.4
        self._document = None
        self._root = None

    def _init(self, code):
        width, height = self.calculate_size(len(code[0]), len(code), self.dpi)
        self._document = create_svg_object()
        self._root = self._document.documentElement
        attributes = dict(width=SIZE.format(width), height=SIZE.format(height))
        _set_attributes(self._root, **attributes)
        self._root.appendChild(self._document.createComment(COMMENT))
        background = self._document.createElement('rect')
        attributes = dict(width='100%', height='100%',
                          style='fill:{0}'.format(self.background))
        _set_attributes(background, **attributes)
        self._root.appendChild(background)

    def _create_module(self, xpos, ypos, width, color):
        element = self._document.createElement('rect')
        attributes = dict(x=SIZE.format(xpos), y=SIZE.format(ypos),
                          width=SIZE.format(width),
                          height=SIZE.format(self.module_height),
                          style='fill:{0};'.format(color))
        _set_attributes(element, **attributes)
        self._root.appendChild(element)

    def _create_text(self, xpos, ypos):
        element = self._document.createElement('text')
        attributes = dict(x=SIZE.format(xpos), y=SIZE.format(ypos),
                          style='fill:{0};font-size:{1}pt;text-anchor:'
                                'middle;'.format(self.foreground,
                                                 self.font_size))
        _set_attributes(element, **attributes)
        text_element = self._document.createTextNode(self.text)
        element.appendChild(text_element)
        self._root.appendChild(element)

    def _finish(self):
        if self.compress:
            return self._document.toxml(encoding='UTF-8')
        else:
            return self._document.toprettyxml(indent=4 * ' ', newl=os.linesep,
                                              encoding='UTF-8')

    def save(self, filename, content):
        """See :py:func:`Interface.save`."""
        if self.compress:
            _filename = '%s.svgz' % filename
            with gzip.open('%s.svgz' % filename, 'wb') as output:
                output.write(content)
        else:
            _filename = '%s.svg' % filename
            with open(_filename, 'wb') as output:
                output.write(content)
        return _filename


if Image is None:
    ImageWriter = None
else:
    class ImageWriter(Base):

        def __init__(self):
            Base.__init__(self, self._init, self._paint_module,
                          self._paint_text, self._finish)
            self.format = 'PNG'
            self.dpi = 300
            self._image = None
            self._draw = None

        def _init(self, code):
            size = self.calculate_size(len(code[0]), len(code), self.dpi)
            self._image = Image.new('RGB', size, self.background)
            self._draw = ImageDraw.Draw(self._image)

        def _paint_module(self, xpos, ypos, width, color):
            size = [(mm2px(xpos, self.dpi), mm2px(ypos, self.dpi)),
                    (mm2px(xpos + width, self.dpi),
                     mm2px(ypos + self.module_height, self.dpi))]
            self._draw.rectangle(size, outline=color, fill=color)

        def _paint_text(self, xpos, ypos):
            font = ImageFont.truetype(FONT, self.font_size * 2)
            width, height = font.getsize(self.text)
            pos = (mm2px(xpos, self.dpi) - width // 2,
                   mm2px(ypos, self.dpi) - height // 4)
            self._draw.text(pos, self.text, font=font, fill=self.foreground)

        def _finish(self):
            return self._image

        def save(self, filename, output):
            """See :py:func:`Interface.save`."""
            filename = '{0}.{1}'.format(filename, self.format.lower())
            output.save(filename, self.format.upper())
            return filename


DEFAULT_WRITER = SVG

DEFAULT_WRITER_OPTIONS = {
    'module_width': 0.2,
    'module_height': 15.0,
    'quiet_zone': 6.5,
    'font_size': 10,
    'text_distance': 5.0,
    'background': 'white',
    'foreground': 'black',
    'write_text': True,
    'text': '',
}
