#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import absolute_import, division, print_function, unicode_literals

__license__   = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import sys, traceback, math
from collections import namedtuple
from functools import wraps, partial
from polyglot.builtins import map, zip

from PyQt5.Qt import (QPaintEngine, QPaintDevice, Qt, QTransform, QBrush)

from calibre.constants import plugins
from calibre.ebooks.pdf.render.serialize import (PDFStream, Path)
from calibre.ebooks.pdf.render.common import inch, A4, fmtnum
from calibre.ebooks.pdf.render.graphics import convert_path, Graphics
from calibre.utils.fonts.sfnt.container import Sfnt, UnsupportedFont
from calibre.utils.fonts.sfnt.metrics import FontMetrics
from polyglot.builtins import codepoint_to_chr, itervalues

Point = namedtuple('Point', 'x y')
ColorState = namedtuple('ColorState', 'color opacity do')
GlyphInfo = namedtuple('GlyphInfo', 'name size stretch positions indices')


def repr_transform(t):
    vals = map(fmtnum, (t.m11(), t.m12(), t.m21(), t.m22(), t.dx(), t.dy()))
    return '[%s]'%' '.join(vals)


def store_error(func):

    @wraps(func)
    def errh(self, *args, **kwargs):
        try:
            func(self, *args, **kwargs)
        except:
            self.errors_occurred = True
            self.errors(traceback.format_exc())

    return errh


class Font(FontMetrics):

    def __init__(self, sfnt):
        FontMetrics.__init__(self, sfnt)
        self.glyph_map = {}


class PdfEngine(QPaintEngine):

    FEATURES = QPaintEngine.AllFeatures & ~(
        QPaintEngine.PorterDuff | QPaintEngine.PerspectiveTransform | QPaintEngine.ObjectBoundingModeGradients | QPaintEngine.RadialGradientFill | QPaintEngine.ConicalGradientFill)  # noqa

    def __init__(self, file_object, page_width, page_height, left_margin,
                 top_margin, right_margin, bottom_margin, width, height,
                 errors=print, debug=print, compress=True,
                 mark_links=False, opts=None, page_margins=(0, 0, 0, 0)):
        QPaintEngine.__init__(self, self.FEATURES)
        self.file_object = file_object
        self.compress, self.mark_links = compress, mark_links
        self.page_height, self.page_width = page_height, page_width
        self.left_margin, self.top_margin = left_margin, top_margin
        self.right_margin, self.bottom_margin = right_margin, bottom_margin
        self.pixel_width, self.pixel_height = width, height
        self.pdf_system = self.create_transform()
        self.graphics = Graphics(self.pixel_width, self.pixel_height)
        self.errors_occurred = False
        self.errors, self.debug = errors, debug
        self.fonts = {}
        self.current_page_num = 1
        self.current_page_inited = False
        self.content_written_to_current_page = False
        self.qt_hack, err = plugins['qt_hack']
        self.has_footers = opts is not None and (opts.pdf_page_numbers or opts.pdf_footer_template is not None)
        self.has_headers = opts is not None and opts.pdf_header_template is not None
        ml, mr, mt, mb = page_margins
        self.header_height = mt
        self.footer_height = mb
        if err:
            raise RuntimeError('Failed to load qt_hack with err: %s'%err)

    def create_transform(self, left_margin=None, top_margin=None, right_margin=None, bottom_margin=None):
        # Setup a co-ordinate transform that allows us to use co-ords
        # from Qt's pixel based co-ordinate system with its origin at the top
        # left corner. PDF's co-ordinate system is based on pts and has its
        # origin in the bottom left corner. We also have to implement the page
        # margins. Therefore, we need to translate, scale and reflect about the
        # x-axis.
        left_margin = self.left_margin if left_margin is None else left_margin
        top_margin = self.top_margin if top_margin is None else top_margin
        right_margin = self.right_margin if right_margin is None else right_margin
        bottom_margin = self.bottom_margin if bottom_margin is None else bottom_margin
        dy = self.page_height - top_margin
        dx = left_margin
        sx =  (self.page_width - left_margin - right_margin) / self.pixel_width
        sy =  (self.page_height - top_margin - bottom_margin) / self.pixel_height
        return QTransform(sx, 0, 0, -sy, dx, dy)

    def apply_graphics_state(self):
        self.graphics(self.pdf_system, self.painter())

    def resolve_fill(self, rect):
        self.graphics.resolve_fill(rect, self.pdf_system,
                                   self.painter().transform())

    @property
    def do_fill(self):
        return self.graphics.current_state.do_fill

    @property
    def do_stroke(self):
        return self.graphics.current_state.do_stroke

    def init_page(self, custom_margins=None):
        self.content_written_to_current_page = False
        if custom_margins is None:
            self.pdf.transform(self.pdf_system)
        else:
            self.pdf.transform(self.create_transform(*custom_margins))
        self.pdf.apply_fill(color=(1, 1, 1))  # QPainter has a default background brush of white
        self.graphics.reset()
        self.pdf.save_stack()
        self.current_page_inited = True

    def begin(self, device):
        if not hasattr(self, 'pdf'):
            try:
                self.pdf = PDFStream(self.file_object, (self.page_width,
                        self.page_height), compress=self.compress,
                                     mark_links=self.mark_links,
                                     debug=self.debug)
                self.graphics.begin(self.pdf)
            except:
                self.errors(traceback.format_exc())
                self.errors_occurred = True
                return False
        return True

    def end_page(self, is_last_page=False):
        if self.current_page_inited:
            self.pdf.restore_stack()
            drop_page = is_last_page and not self.content_written_to_current_page
            self.pdf.end_page(drop_page=drop_page)
            self.current_page_inited = False
            self.current_page_num += 0 if drop_page else 1
        return self.content_written_to_current_page

    def end(self):
        try:
            self.end_page()
            self.pdf.end()
        except:
            self.errors(traceback.format_exc())
            self.errors_occurred = True
            return False
        finally:
            self.pdf = self.file_object = None
        return True

    def type(self):
        return QPaintEngine.Pdf

    def add_image(self, img, cache_key):
        if img.isNull():
            return
        return self.pdf.add_image(img, cache_key)

    @store_error
    def drawTiledPixmap(self, rect, pixmap, point):
        self.content_written_to_current_page = 'drawTiledPixmap'
        self.apply_graphics_state()
        brush = QBrush(pixmap)
        bl = rect.topLeft()
        color, opacity, pattern, do_fill = self.graphics.convert_brush(
            brush, bl-point, 1.0, self.pdf_system,
            self.painter().transform())
        self.pdf.save_stack()
        self.pdf.apply_fill(color, pattern)
        self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
                            stroke=False, fill=True)
        self.pdf.restore_stack()

    @store_error
    def drawPixmap(self, rect, pixmap, source_rect):
        self.content_written_to_current_page = 'drawPixmap'
        self.apply_graphics_state()
        source_rect = source_rect.toRect()
        pixmap = (pixmap if source_rect == pixmap.rect() else
                  pixmap.copy(source_rect))
        image = pixmap.toImage()
        ref = self.add_image(image, pixmap.cacheKey())
        if ref is not None:
            self.pdf.draw_image(rect.x(), rect.y(), rect.width(),
                                rect.height(), ref)

    @store_error
    def drawImage(self, rect, image, source_rect, flags=Qt.AutoColor):
        self.content_written_to_current_page = 'drawImage'
        self.apply_graphics_state()
        source_rect = source_rect.toRect()
        image = (image if source_rect == image.rect() else
                 image.copy(source_rect))
        ref = self.add_image(image, image.cacheKey())
        if ref is not None:
            self.pdf.draw_image(rect.x(), rect.y(), rect.width(),
                                rect.height(), ref)

    @store_error
    def updateState(self, state):
        self.graphics.update_state(state, self.painter())

    @store_error
    def drawPath(self, path):
        self.content_written_to_current_page = 'drawPath'
        self.apply_graphics_state()
        p = convert_path(path)
        fill_rule = {Qt.OddEvenFill:'evenodd',
                    Qt.WindingFill:'winding'}[path.fillRule()]
        self.pdf.draw_path(p, stroke=self.do_stroke,
                                fill=self.do_fill, fill_rule=fill_rule)

    @store_error
    def drawPoints(self, points):
        self.content_written_to_current_page = 'drawPoints'
        self.apply_graphics_state()
        p = Path()
        for point in points:
            p.move_to(point.x(), point.y())
            p.line_to(point.x(), point.y() + 0.001)
        self.pdf.draw_path(p, stroke=self.do_stroke, fill=False)

    @store_error
    def drawRects(self, rects):
        self.apply_graphics_state()
        with self.graphics:
            for rect in rects:
                self.resolve_fill(rect)
                bl = rect.topLeft()
                if self.do_stroke or self.do_fill:
                    self.content_written_to_current_page = 'drawRects'
                self.pdf.draw_rect(bl.x(), bl.y(), rect.width(), rect.height(),
                                stroke=self.do_stroke, fill=self.do_fill)

    def create_sfnt(self, text_item):
        get_table = partial(self.qt_hack.get_sfnt_table, text_item)
        try:
            ans = Font(Sfnt(get_table))
        except UnsupportedFont as e:
            raise UnsupportedFont('The font %s is not a valid sfnt. Error: %s'%(
                text_item.font().family(), e))
        glyph_map = self.qt_hack.get_glyph_map(text_item)
        gm = {}
        ans.ignore_glyphs = set()
        for uc, glyph_id in enumerate(glyph_map):
            if glyph_id not in gm:
                gm[glyph_id] = codepoint_to_chr(uc)
                if uc in (0xad, 0x200b):
                    ans.ignore_glyphs.add(glyph_id)
        ans.full_glyph_map = gm
        return ans

    @store_error
    def drawTextItem(self, point, text_item):
        # return super(PdfEngine, self).drawTextItem(point, text_item)
        self.apply_graphics_state()
        gi = GlyphInfo(*self.qt_hack.get_glyphs(point, text_item))
        if not gi.indices:
            return
        metrics = self.fonts.get(gi.name)
        if metrics is None:
            from calibre.utils.fonts.utils import get_all_font_names
            try:
                names = get_all_font_names(gi.name, True)
                names = ' '.join('%s=%s'%(k, names[k]) for k in sorted(names))
            except Exception:
                names = 'Unknown'
            self.debug('Loading font: %s' % names)
            try:
                self.fonts[gi.name] = metrics = self.create_sfnt(text_item)
            except UnsupportedFont:
                self.debug('Failed to load font: %s, drawing text as outlines...' % names)
                return super(PdfEngine, self).drawTextItem(point, text_item)
        indices, positions = [], []
        ignore_glyphs = metrics.ignore_glyphs
        for glyph_id, gpos in zip(gi.indices, gi.positions):
            if glyph_id not in ignore_glyphs:
                indices.append(glyph_id), positions.append(gpos)
        for glyph_id in indices:
            try:
                metrics.glyph_map[glyph_id] = metrics.full_glyph_map[glyph_id]
            except (KeyError, ValueError):
                pass
        glyphs = []
        last_x = last_y = 0
        for glyph_index, (x, y) in zip(indices, positions):
            glyphs.append((x-last_x, last_y - y, glyph_index))
            last_x, last_y = x, y

        if not self.content_written_to_current_page:
            dy = self.graphics.current_state.transform.dy()
            ypositions = [y + dy for x, y in positions]
            miny = min(ypositions or (0,))
            maxy = max(ypositions or (self.pixel_height,))
            page_top = self.header_height if self.has_headers else 0
            page_bottom = self.pixel_height - (self.footer_height if self.has_footers else 0)
            if page_top <= miny <= page_bottom or page_top <= maxy <= page_bottom:
                self.content_written_to_current_page = 'drawTextItem'
            else:
                self.debug('Text in header/footer: miny=%s maxy=%s page_top=%s page_bottom=%s'% (
                    miny, maxy, page_top, page_bottom))
        self.pdf.draw_glyph_run([gi.stretch, 0, 0, -1, 0, 0], gi.size, metrics,
                                glyphs)

    @store_error
    def drawPolygon(self, points, mode):
        self.content_written_to_current_page = 'drawPolygon'
        self.apply_graphics_state()
        if not points:
            return
        p = Path()
        p.move_to(points[0].x(), points[0].y())
        for point in points[1:]:
            p.line_to(point.x(), point.y())
        p.close()
        fill_rule = {self.OddEvenMode:'evenodd',
                    self.WindingMode:'winding'}.get(mode, 'evenodd')
        self.pdf.draw_path(p, stroke=True, fill_rule=fill_rule,
            fill=(mode in (self.OddEvenMode, self.WindingMode, self.ConvexMode)))

    def set_metadata(self, *args, **kwargs):
        self.pdf.set_metadata(*args, **kwargs)

    def add_outline(self, toc):
        self.pdf.links.add_outline(toc)

    def add_links(self, current_item, start_page, links, anchors):
        for pos in itervalues(anchors):
            pos['left'], pos['top'] = self.pdf_system.map(pos['left'], pos['top'])
        for link in links:
            pos = link[1]
            llx = pos['left']
            lly = pos['top'] + pos['height']
            urx = pos['left'] + pos['width']
            ury = pos['top']
            llx, lly = self.pdf_system.map(llx, lly)
            urx, ury = self.pdf_system.map(urx, ury)
            link[1] = pos['column'] + start_page
            link.append((llx, lly, urx, ury))
        self.pdf.links.add(current_item, start_page, links, anchors)


class PdfDevice(QPaintDevice):  # {{{

    def __init__(self, file_object, page_size=A4, left_margin=inch,
                 top_margin=inch, right_margin=inch, bottom_margin=inch,
                 xdpi=1200, ydpi=1200, errors=print, debug=print,
                 compress=True, mark_links=False, opts=None, page_margins=(0, 0, 0, 0)):
        QPaintDevice.__init__(self)
        self.xdpi, self.ydpi = xdpi, ydpi
        self.page_width, self.page_height = page_size
        self.body_width = self.page_width - left_margin - right_margin
        self.body_height = self.page_height - top_margin - bottom_margin
        self.left_margin, self.right_margin = left_margin, right_margin
        self.top_margin, self.bottom_margin = top_margin, bottom_margin
        self.engine = PdfEngine(file_object, self.page_width, self.page_height,
                                left_margin, top_margin, right_margin,
                                bottom_margin, self.width(), self.height(),
                                errors=errors, debug=debug, compress=compress,
                                mark_links=mark_links, opts=opts, page_margins=page_margins)
        self.add_outline = self.engine.add_outline
        self.add_links = self.engine.add_links

    def paintEngine(self):
        return self.engine

    def metric(self, m):
        if m in (self.PdmDpiX, self.PdmPhysicalDpiX):
            return self.xdpi
        if m in (self.PdmDpiY, self.PdmPhysicalDpiY):
            return self.ydpi
        if m == self.PdmDepth:
            return 32
        if m == self.PdmNumColors:
            return sys.maxsize
        if m == self.PdmWidthMM:
            return int(round(self.body_width * 0.35277777777778))
        if m == self.PdmHeightMM:
            return int(round(self.body_height * 0.35277777777778))
        if m == self.PdmWidth:
            return int(round(self.body_width * self.xdpi / 72.0))
        if m == self.PdmHeight:
            return int(round(self.body_height * self.ydpi / 72.0))
        return 0

    def end_page(self, *args, **kwargs):
        self.engine.end_page(*args, **kwargs)

    def init_page(self, custom_margins=None):
        self.engine.init_page(custom_margins=custom_margins)

    @property
    def full_page_rect(self):
        page_width = int(math.ceil(self.page_width * self.xdpi / 72.0))
        lm = int(math.ceil(self.left_margin * self.xdpi / 72.0))
        page_height = int(math.ceil(self.page_height * self.ydpi / 72.0))
        tm = int(math.ceil(self.top_margin * self.ydpi / 72.0))
        return (-lm, -tm, page_width+1, page_height+1)

    @property
    def current_page_num(self):
        return self.engine.current_page_num

    @property
    def errors_occurred(self):
        return self.engine.errors_occurred

    def to_px(self, pt, vertical=True):
        return pt * (self.height()/self.page_height if vertical else
                     self.width()/self.page_width)

    def to_pt(self, px, vertical=True):
        return px * (self.page_height / self.height() if vertical else
                self.page_width / self.width())

    def set_metadata(self, *args, **kwargs):
        self.engine.set_metadata(*args, **kwargs)

# }}}
