# -*- coding: utf-8 -*-
#
#  Copyright (C) 2016 by Igor E. Novikov
#
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

import math
from PIL import Image
from base64 import b64decode
from cStringIO import StringIO
from copy import deepcopy
from reportlab.lib.colors import CMYKColorSep, Color, CMYKColor
from reportlab.lib.utils import ImageReader
from reportlab.pdfbase.pdfdoc import PDFInfo, PDFString, PDFDate, PDFDictionary
from reportlab.pdfgen.canvas import Canvas, FILL_EVEN_ODD, FILL_NON_ZERO

from pdfconst import PDF_VERSION_DEFAULT
from uc2 import _, uc2const, events
from uc2 import libgeom, sk2const, libimg
from uc2.formats.sk2 import sk2_model


class UC_PDFInfo(PDFInfo):
    pdfxversion = 'PDF/X-4'

    def __init__(self, pdfdoc):
        PDFInfo.__init__(self)
        pdfdoc.info = self
        self.invariant = pdfdoc.invariant

    def format(self, document):
        D = {}
        D["Title"] = PDFString(self.title)
        D["Author"] = PDFString(self.author)
        D["CreationDate"] = PDFDate(invariant=self.invariant,
                                    dateFormatter=self._dateFormatter)
        D["Producer"] = PDFString(self.producer)
        if self.pdfxversion:
            D["GTS_PDFXVersion"] = PDFString(self.pdfxversion)
        D["Creator"] = PDFString(self.creator)
        D["Subject"] = PDFString(self.subject)
        D["Keywords"] = PDFString(self.keywords)

        PD = PDFDictionary(D)
        return PD.format(document)


class PDFGenerator(object):
    canvas = None
    colorspace = None
    use_spot = True
    num_pages = 0
    page_count = 0
    prgs_msg = _('Saving in process...')

    def __init__(self, fileptr, cms, version=PDF_VERSION_DEFAULT):
        self.cms = cms
        self.canvas = Canvas(fileptr, pdfVersion=version[0])
        self.info = UC_PDFInfo(self.canvas._doc)
        self.info.pdfxversion = version[1]
        self.info.subject = '---'
        self.canvas.setPageCompression(1)

    # ---PDF doc data
    def set_creator(self, name):
        self.info.creator = name

    def set_producer(self, name):
        self.info.producer = name

    def set_title(self, title):
        self.info.title = title

    def set_author(self, author):
        self.info.author = author

    def set_subject(self, subj):
        self.info.subject = subj

    def set_keywords(self, keywords):
        self.info.keywords = keywords

    # ---Rendering options
    def set_compression(self, val=True):
        self.canvas.setPageCompression(int(val))

    def set_colorspace(self, cs=None):
        self.colorspace = cs

    def set_spot_usage(self, val=True):
        self.use_spot = val

    # ---Page processing

    def set_num_pages(self, num=1):
        self.num_pages = num

    def set_progress_message(self, msg):
        self.prgs_msg = msg

    def start_page(self, w, h, left_margin=0.0, top_margin=0.0):
        self.canvas.translate(w / 2.0 - left_margin, h / 2.0 - top_margin)
        self.canvas.setPageSize((w, h))
        position = 0.0
        if self.num_pages:
            position = float(self.page_count) / float(self.num_pages)
        events.emit(events.FILTER_INFO, self.prgs_msg, position)

    def end_page(self):
        self.canvas.showPage()
        self.page_count += 1
        position = 1.0
        if self.num_pages:
            position = float(self.page_count) / float(self.num_pages)
        events.emit(events.FILTER_INFO, self.prgs_msg, position)

    def save(self):
        self.canvas.save()

    # --- Rendering
    def render(self, objs, toplevel=False):
        obj_count = 0
        for obj in objs:
            if obj.is_pixmap():
                self.draw_pixmap(obj)
            elif obj.is_primitive():
                curve_obj = obj.to_curve()
                if curve_obj.is_primitive():
                    self.draw_curve(curve_obj)
                else:
                    self.render(curve_obj.childs)
            elif obj.is_container():
                self.draw_container(obj)
            else:
                self.render(obj.childs)

            # ---Progress
            obj_count += 1
            shift = 0.0
            page_size = 1.0
            if self.num_pages:
                shift = float(self.page_count) / float(self.num_pages)
                page_size = 1.0 / float(self.num_pages)
            position = shift + obj_count / len(objs) * page_size
            events.emit(events.FILTER_INFO, self.prgs_msg, position)

    def draw_curve(self, curve_obj):
        paths = libgeom.apply_trafo_to_paths(curve_obj.paths, curve_obj.trafo)
        pdfpath, closed = self.make_pdfpath(paths)
        fill_style = curve_obj.style[0]
        stroke_style = curve_obj.style[1]
        if stroke_style and stroke_style[7]:
            self.stroke_pdfpath(pdfpath, stroke_style, curve_obj.stroke_trafo)
        if fill_style and fill_style[0] & sk2const.FILL_CLOSED_ONLY and closed:
            self.fill_pdfpath(curve_obj, pdfpath, fill_style,
                              curve_obj.fill_trafo)
        elif fill_style and not fill_style[0] & sk2const.FILL_CLOSED_ONLY:
            self.fill_pdfpath(curve_obj, pdfpath, fill_style,
                              curve_obj.fill_trafo)
        if stroke_style and not stroke_style[7]:
            self.stroke_pdfpath(pdfpath, stroke_style, curve_obj.stroke_trafo)

    def draw_container(self, obj):
        container = obj.childs[0].to_curve()
        paths = libgeom.apply_trafo_to_paths(container.paths, container.trafo)
        pdfpath, closed = self.make_pdfpath(paths)
        fill_style = container.style[0]
        stroke_style = container.style[1]
        if stroke_style and stroke_style[7]:
            self.stroke_pdfpath(pdfpath, stroke_style, container.stroke_trafo)

        self.canvas.saveState()
        self.canvas.clipPath(pdfpath, 0, 0)

        if fill_style and fill_style[0] & sk2const.FILL_CLOSED_ONLY and closed:
            self.fill_pdfpath(container, pdfpath, fill_style,
                              container.fill_trafo)
        elif fill_style and not fill_style[0] & sk2const.FILL_CLOSED_ONLY:
            self.fill_pdfpath(container, pdfpath, fill_style,
                              container.fill_trafo)

        self.render(obj.childs[1:])

        self.canvas.restoreState()

        if stroke_style and not stroke_style[7]:
            self.stroke_pdfpath(pdfpath, stroke_style, container.stroke_trafo)

    def make_pdfpath(self, paths):
        closed = False
        pdfpath = self.canvas.beginPath()
        for path in paths:
            pdfpath.moveTo(*path[0])
            for point in path[1]:
                if len(point) > 2:
                    pdfpath.curveTo(point[0][0], point[0][1],
                                    point[1][0], point[1][1],
                                    point[2][0], point[2][1])
                else:
                    pdfpath.lineTo(*point)
            if path[2]:
                pdfpath.close()
                closed = True
        return pdfpath, closed

    def set_fill_rule(self, fillrule):
        if fillrule in (sk2const.FILL_EVENODD,
                        sk2const.FILL_EVENODD_CLOSED_ONLY):
            fillrule = FILL_EVEN_ODD
        else:
            fillrule = FILL_NON_ZERO
        self.canvas._fillMode = fillrule

    def set_rgb_values(self, color, pdfcolor):
        r, g, b = self.cms.get_rgb_color(color)[1]
        density = pdfcolor.density
        if density < 1:
            r = density * (r - 1) + 1
            g = density * (g - 1) + 1
            b = density * (b - 1) + 1
        pdfcolor.red, pdfcolor.green, pdfcolor.blue = (r, g, b)

    def get_pdfcolor(self, color):
        pdfcolor = None
        alpha = color[2]
        if self.use_spot and color[0] == uc2const.COLOR_SPOT:
            c, m, y, k = self.cms.get_cmyk_color(color)[1]
            spotname = color[3]
            if spotname == uc2const.COLOR_REG: spotname = 'All'
            pdfcolor = CMYKColorSep(c, m, y, k, spotName=spotname, alpha=alpha)
        elif self.colorspace == uc2const.COLOR_CMYK:
            c, m, y, k = self.cms.get_cmyk_color(color)[1]
            pdfcolor = CMYKColor(c, m, y, k, alpha=alpha)
        elif self.colorspace == uc2const.COLOR_RGB:
            r, g, b = self.cms.get_rgb_color(color)[1]
            return Color(r, g, b, alpha)
        elif self.colorspace == uc2const.COLOR_GRAY:
            gray = self.cms.get_grayscale_color(color)
            k = 1.0 - gray[1][0]
            c = m = y = 0.0
            pdfcolor = CMYKColor(c, m, y, k, alpha=alpha)
        else:
            if color[0] == uc2const.COLOR_RGB:
                r, g, b = color[1]
                return Color(r, g, b, alpha)
            elif color[0] == uc2const.COLOR_GRAY:
                k = 1.0 - color[1][0]
                c = m = y = 0.0
                pdfcolor = CMYKColor(c, m, y, k, alpha=alpha)
            else:
                c, m, y, k = self.cms.get_cmyk_color(color)[1]
                pdfcolor = CMYKColor(c, m, y, k, alpha=alpha)

        self.set_rgb_values(color, pdfcolor)
        return pdfcolor

    def stroke_pdfpath(self, pdfpath, stroke_style, stroke_trafo=[]):
        width = stroke_style[1]

        if not stroke_style[8]:
            width = stroke_style[1]
        else:
            if not stroke_trafo:
                stroke_trafo = [] + sk2const.NORMAL_TRAFO
            points = [[0.0, 0.0], [1.0, 0.0]]
            points = libgeom.apply_trafo_to_points(points, stroke_trafo)
            coef = libgeom.distance(*points)
            width = stroke_style[1] * coef

        self.canvas.setStrokeColor(self.get_pdfcolor(stroke_style[2]))
        dash = stroke_style[3]
        caps = stroke_style[4]
        joint = stroke_style[5]
        miter = stroke_style[6]

        self.canvas.setLineWidth(width)
        self.canvas.setLineCap(caps - 1)
        self.canvas.setLineJoin(joint)
        dashes = []
        if dash:
            dashes = list(dash)
            w = width
            if w < 1.0: w = 1.0
            for i in range(len(dashes)):
                dashes[i] = w * dashes[i]
        self.canvas.setDash(dashes)
        self.canvas.setMiterLimit(miter)
        self.canvas.drawPath(pdfpath, 1, 0)
        self.canvas.setStrokeAlpha(1.0)

    def fill_pdfpath(self, obj, pdfpath, fill_style, fill_trafo=None):
        self.set_fill_rule(fill_style[0])

        if fill_style[1] == sk2const.FILL_SOLID:
            self.canvas.setFillColor(self.get_pdfcolor(fill_style[2]))
            self.canvas.drawPath(pdfpath, 0, 1)
        elif fill_style[1] == sk2const.FILL_GRADIENT:
            gradient = fill_style[2]
            stops = gradient[2]
            transparency = False
            for stop in stops:
                if stop[1][2] < 1.0:
                    transparency = True
                    break
            if transparency:
                self.fill_tr_gradient(obj, pdfpath, fill_trafo, gradient)
            else:
                self.fill_gradient(pdfpath, fill_trafo, gradient)

        elif fill_style[1] == sk2const.FILL_PATTERN:
            pattern = fill_style[2]
            self.fill_pattern(obj, pdfpath, fill_trafo, pattern)

    def fill_gradient(self, pdfpath, fill_trafo, gradient):
        self.canvas.saveState()
        self.canvas.clipPath(pdfpath, 0, 0)
        if fill_trafo:
            self.canvas.transform(*fill_trafo)
        grad_type = gradient[0]
        sp, ep = gradient[1]
        stops = gradient[2]
        colors = []
        positions = []
        for offset, color in stops:
            positions.append(offset)
            colors.append(self.get_pdfcolor(color))
        if grad_type == sk2const.GRADIENT_RADIAL:
            radius = libgeom.distance(sp, ep)
            self.canvas.radialGradient(sp[0], sp[1], radius, colors,
                                       positions, True)
        else:
            x0, y0 = sp
            x1, y1 = ep
            self.canvas.linearGradient(x0, y0, x1, y1, colors,
                                       positions, True)
        self.canvas.restoreState()

    def fill_tr_gradient(self, obj, pdfpath, fill_trafo, gradient):
        grad_type = gradient[0]
        if grad_type == sk2const.GRADIENT_RADIAL:
            self.fill_radial_tr_gradient(obj, pdfpath, fill_trafo, gradient)
        else:
            self.fill_linear_tr_gradient(obj, pdfpath, fill_trafo, gradient)

    def get_grcolor_at_point(self, stops, point=0.0):
        if not point: return self.get_pdfcolor(stops[0][1])
        if point == 1.0: return self.get_pdfcolor(stops[-1][1])
        stop0 = stops[0]
        stop1 = None
        for item in stops:
            if item[0] < point: stop0 = item
            if item[0] >= point:
                stop1 = item
                break
        size = stop1[0] - stop0[0]
        if not size:
            color = stop1[1]
        else:
            coef = (point - stop0[0]) / size
            color = self.cms.mix_colors(stop0[1], stop1[1], coef)
        return self.get_pdfcolor(color)

    def fill_linear_tr_gradient(self, obj, pdfpath, fill_trafo, gradient):
        if not fill_trafo:
            fill_trafo = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
        stops = gradient[2]
        sp, ep = gradient[1]
        dx, dy = sp
        l = libgeom.distance(sp, ep)
        angle = libgeom.get_point_angle(ep, sp)
        m21 = math.sin(angle)
        m11 = m22 = math.cos(angle)
        m12 = -m21
        trafo = [m11, m21, m12, m22, dx, dy]
        inv_trafo = libgeom.multiply_trafo(libgeom.invert_trafo(fill_trafo),
                                           libgeom.invert_trafo(trafo))
        cv_trafo = libgeom.multiply_trafo(trafo, fill_trafo)
        paths = libgeom.apply_trafo_to_paths(obj.paths, obj.trafo)
        paths = libgeom.apply_trafo_to_paths(paths, inv_trafo)
        bbox = libgeom.sum_bbox(libgeom.get_paths_bbox(paths),
                                [0.0, 0.0, l, 0.0])
        bbox = libgeom.normalize_bbox(bbox)

        y = bbox[1]
        d = libgeom.distance(*libgeom.apply_trafo_to_points([[0.0, 0.0],
                                                             [0.0, 1.0]],
                                                            inv_trafo))
        height = bbox[3] - bbox[1]

        self.canvas.saveState()
        self.canvas.clipPath(pdfpath, 0, 0)
        self.canvas.transform(*cv_trafo)

        self.canvas.setFillColor(self.get_grcolor_at_point(stops, 0.0))
        self.canvas.rect(bbox[0], y, 0.0 - bbox[0], height, stroke=0, fill=1)

        x = 0.0
        while x < l:
            point = x / l
            self.canvas.setFillColor(self.get_grcolor_at_point(stops, point))
            if x + d < l:
                width = d
            else:
                width = l - x
            self.canvas.rect(x, y, width, height, stroke=0, fill=1)
            x += d

        self.canvas.setFillColor(self.get_grcolor_at_point(stops, 1.0))
        self.canvas.rect(l, y, bbox[2] - l, height, stroke=0, fill=1)

        self.canvas.restoreState()

    def fill_radial_tr_gradient(self, obj, pdfpath, fill_trafo, gradient):
        if not fill_trafo:
            fill_trafo = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
        stops = gradient[2]
        sp, ep = gradient[1]
        dx, dy = sp
        l = libgeom.distance(sp, ep)
        trafo = [1.0, 0.0, 0.0, 1.0, dx, dy]
        inv_trafo = libgeom.multiply_trafo(libgeom.invert_trafo(fill_trafo),
                                           libgeom.invert_trafo(trafo))
        cv_trafo = libgeom.multiply_trafo(trafo, fill_trafo)
        paths = libgeom.apply_trafo_to_paths(obj.paths, obj.trafo)
        paths = libgeom.apply_trafo_to_paths(paths, inv_trafo)
        bbox = libgeom.sum_bbox(libgeom.get_paths_bbox(paths),
                                [0.0, 0.0, l, 0.0])
        bbox = libgeom.normalize_bbox(bbox)
        d = libgeom.distance(*libgeom.apply_trafo_to_points([[0.0, 0.0],
                                                             [0.0, 1.0]],
                                                            inv_trafo))

        circle_paths = libgeom.get_circle_paths(0.0, 0.0, sk2const.ARC_CHORD)
        trafo = [2.0, 0.0, 0.0, 2.0, -1.0, -1.0]
        circle_paths = libgeom.apply_trafo_to_paths(circle_paths, trafo)

        inner_paths = []
        r = 0.0
        self.canvas.saveState()
        self.canvas.clipPath(pdfpath, 0, 0)
        self.canvas.transform(*cv_trafo)
        while r < l:
            point = r / l
            self.canvas.setFillColor(self.get_grcolor_at_point(stops, point))
            if r + d < l:
                coef = (r + d)
            else:
                coef = l
            trafo = [coef, 0.0, 0.0, coef, 0.0, 0.0]
            paths = libgeom.apply_trafo_to_paths(circle_paths, trafo)
            ring = self.make_pdfpath(inner_paths + paths)[0]
            inner_paths = paths
            self.canvas.drawPath(ring, stroke=0, fill=1)
            r += d

        self.canvas.setFillColor(self.get_grcolor_at_point(stops, 1.0))
        r = max(bbox[2] - bbox[0], bbox[3] - bbox[1])
        trafo = [2.0 * r, 0.0, 0.0, 2.0 * r, 0.0, 0.0]
        paths = libgeom.apply_trafo_to_paths(circle_paths, trafo)
        ring = self.make_pdfpath(inner_paths + paths)[0]
        self.canvas.drawPath(ring, stroke=0, fill=1)

        self.canvas.restoreState()

    def draw_image(self, image, alpha_channel=None):
        if not image: return
        if self.colorspace == uc2const.COLOR_CMYK:
            image = self.cms.convert_image(image, uc2const.IMAGE_CMYK)
        elif self.colorspace == uc2const.COLOR_RGB:
            image = self.cms.convert_image(image, uc2const.IMAGE_RGB)
        elif self.colorspace == uc2const.COLOR_GRAY:
            image = self.cms.convert_image(image, uc2const.IMAGE_GRAY)
        img = ImageReader(image)
        img.getRGBData()
        if alpha_channel: img._dataA = ImageReader(alpha_channel)
        self.canvas.drawImage(img, 0, 0, mask='auto')

    def draw_pixmap_obj(self, obj):
        if obj.colorspace in uc2const.DUOTONES:
            fg, bg = libimg.convert_duotone_to_image(self.cms, obj)
            self.draw_image(*bg)
            self.draw_image(*fg)
        else:
            raw_image = Image.open(StringIO(obj.bitmap))
            raw_image.load()
            alpha_chnl = None
            if obj.alpha_channel:
                alpha_chnl = Image.open(StringIO(obj.alpha_channel))
                alpha_chnl.load()
            self.draw_image(raw_image, alpha_chnl)

    def draw_pixmap(self, obj):
        self.canvas.saveState()
        self.canvas.transform(*obj.trafo)
        self.canvas.setFillColorCMYK(0, 0, 0, 1, 1)
        self.canvas.setStrokeColorCMYK(0, 0, 0, 1, 1)
        self.draw_pixmap_obj(obj)
        self.canvas.restoreState()

    def fill_pattern(self, obj, pdfpath, fill_trafo, pattern):
        if not fill_trafo:
            fill_trafo = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]
        inv_ptrn_trafo = libgeom.invert_trafo(pattern[3])
        inv_trafo = libgeom.multiply_trafo(libgeom.invert_trafo(fill_trafo),
                                           libgeom.invert_trafo(inv_ptrn_trafo))
        paths = libgeom.apply_trafo_to_paths(obj.paths, obj.trafo)
        paths = libgeom.apply_trafo_to_paths(paths, inv_trafo)
        bbox = libgeom.get_paths_bbox(paths)
        cv_trafo = libgeom.multiply_trafo(pattern[3], fill_trafo)

        bmpstr = b64decode(pattern[1])
        image_obj = sk2_model.Pixmap(obj.config)
        libimg.set_image_data(self.cms, image_obj, bmpstr)
        if pattern[0] == sk2const.PATTERN_IMG and \
                        len(pattern) > 2:
            image_obj.style[3] = deepcopy(pattern[2])
        libimg.update_image(self.cms, image_obj)

        self.canvas.saveState()
        self.canvas.clipPath(pdfpath, 0, 0)
        self.canvas.transform(*cv_trafo)

        w, h = image_obj.get_size()
        x = bbox[0]
        y = bbox[3]
        while y > bbox[1] - h:
            while x < bbox[2]:
                self.canvas.saveState()
                self.canvas.transform(1.0, 0.0, 0.0, 1.0, x, y)
                self.draw_pixmap_obj(image_obj)
                self.canvas.restoreState()
                x += w
            y -= h
            x = bbox[0]
        self.canvas.restoreState()
