# -*- coding: utf-8 -*-
#
#  Copyright (C) 2011-2015 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 cairo
from base64 import b64decode
from copy import deepcopy

from uc2 import libimg, libcairo, libgeom, sk2const
from uc2.formats.sk2 import sk2_model

CAIRO_BLACK = [0.0, 0.0, 0.0]
CAIRO_GRAY = [0.5, 0.5, 0.5]
CAIRO_WHITE = [1.0, 1.0, 1.0]

CAPS = {
    sk2const.CAP_BUTT: cairo.LINE_CAP_BUTT,
    sk2const.CAP_ROUND: cairo.LINE_CAP_ROUND,
    sk2const.CAP_SQUARE: cairo.LINE_CAP_SQUARE,
}

JOINS = {
    sk2const.JOIN_BEVEL: cairo.LINE_JOIN_BEVEL,
    sk2const.JOIN_MITER: cairo.LINE_JOIN_MITER,
    sk2const.JOIN_ROUND: cairo.LINE_JOIN_ROUND,
}

EXTEND = {
    sk2const.GRADIENT_EXTEND_NONE: cairo.EXTEND_NONE,
    sk2const.GRADIENT_EXTEND_PAD: cairo.EXTEND_PAD,
    sk2const.GRADIENT_EXTEND_REPEAT: cairo.EXTEND_REPEAT,
    sk2const.GRADIENT_EXTEND_REFLECT: cairo.EXTEND_REFLECT,
}


class CairoRenderer:
    cms = None
    antialias_flag = True
    contour_flag = False
    stroke_style = []
    for_display = False

    def __init__(self, cms):
        self.cms = cms

    # -------ROUTINES----------

    def get_color(self, color):
        """
        Provides Cairo suitable, CMS processed color values. 
        """
        if self.for_display:
            r, g, b = self.cms.get_display_color(color)
        else:
            r, g, b = self.cms.get_rgb_color(color)[1]
        return r, g, b, color[2]

    def get_image(self, pixmap):
        if self.cms.proofing:
            if not pixmap.cache_ps_cdata:
                libimg.update_image(self.cms, pixmap)
                pixmap.cache_ps_cdata = pixmap.cache_cdata
                pixmap.cache_cdata = None
            return pixmap.cache_ps_cdata
        else:
            if not pixmap.cache_cdata:
                libimg.update_image(self.cms, pixmap)
            return pixmap.cache_cdata

    def _create_pattern_image(self, obj, force_proofing=False, gray=False):
        fill = obj.style[0]
        pattern_fill = fill[2]
        bmpstr = b64decode(pattern_fill[1])
        image_obj = sk2_model.Pixmap(obj.config)
        libimg.set_image_data(self.cms, image_obj, bmpstr)
        libimg.flip_top_to_bottom(image_obj)
        if pattern_fill[0] == sk2const.PATTERN_IMG and len(pattern_fill) > 2:
            image_obj.style[3] = deepcopy(pattern_fill[2])
        if gray:
            libimg.update_gray_image(self.cms, image_obj)
        else:
            libimg.update_image(self.cms, image_obj, force_proofing)
        return image_obj

    def get_pattern_image(self, obj):
        if self.cms.proofing:
            if not obj.cache_ps_pattern_img:
                image_obj = self._create_pattern_image(obj)
                obj.cache_ps_pattern_img = image_obj.cache_cdata
            return obj.cache_ps_pattern_img
        else:
            if not obj.cache_pattern_img:
                image_obj = self._create_pattern_image(obj)
                obj.cache_pattern_img = image_obj.cache_cdata
            return obj.cache_pattern_img

    # -------DOCUMENT RENDERING

    def render(self, ctx, objs=None):
        objs = objs or []
        if self.antialias_flag:
            ctx.set_antialias(cairo.ANTIALIAS_DEFAULT)
        else:
            ctx.set_antialias(cairo.ANTIALIAS_NONE)

        if objs:
            for obj in objs:
                self.render_object(ctx, obj)

    def render_object(self, ctx, obj):
        if obj.is_primitive():
            self.render_primitives(ctx, obj)
        elif obj.is_container():
            self.render_container(ctx, obj)
        elif obj.is_group():
            for obj in obj.childs:
                self.render_object(ctx, obj)
        else:
            pass

    def render_container(self, ctx, obj):
        ctx.save()
        container = obj.cache_container

        if container.style[1] and not self.contour_flag \
                and container.style[1][7]:
            ctx.new_path()
            self.process_stroke(ctx, None, container.style)
            ctx.append_path(container.cache_cpath)
            ctx.stroke()

        if container.style[0] and not self.contour_flag:
            ctx.new_path()
            self.process_fill(ctx, container)
            ctx.append_path(container.cache_cpath)
            ctx.fill()

        ctx.new_path()
        ctx.append_path(container.cache_cpath)
        ctx.clip()
        for obj in obj.childs[1:]:
            self.render_object(ctx, obj)
        ctx.restore()

        if container.style[1] and not self.contour_flag and \
                not container.style[1][7]:
            ctx.new_path()
            self.process_stroke(ctx, None, container.style)
            ctx.append_path(container.cache_cpath)
            ctx.stroke()
        elif self.contour_flag:
            ctx.new_path()
            self.process_stroke(ctx, None, self.stroke_style)
            ctx.append_path(container.cache_cpath)
            ctx.stroke()

    def render_image(self, ctx, obj):
        canvas_matrix = ctx.get_matrix()
        canvas_trafo = libcairo.get_trafo_from_matrix(canvas_matrix)
        zoom = canvas_trafo[0]

        h = obj.size[1]
        lu_corner = libgeom.apply_trafo_to_point([0.0, float(h)], obj.trafo)
        x0, y0 = libgeom.apply_trafo_to_point(lu_corner, canvas_trafo)

        m11, m12, m21, m22 = obj.trafo[:4]
        matrix = cairo.Matrix(zoom * m11, -zoom * m12,
                              - zoom * m21, zoom * m22, x0, y0)
        ctx.set_matrix(matrix)

        if self.contour_flag:
            if not obj.cache_gray_cdata:
                libimg.update_gray_image(self.cms, obj)
            ctx.set_source_surface(obj.cache_gray_cdata)
            if zoom * abs(m11) > .98:
                ctx.get_source().set_filter(cairo.FILTER_NEAREST)
            ctx.paint_with_alpha(0.3)
        else:
            ctx.set_source_surface(self.get_image(obj))
            if zoom * abs(m11) > .98:
                ctx.get_source().set_filter(cairo.FILTER_NEAREST)
            ctx.paint()

        ctx.set_matrix(canvas_matrix)

    def render_primitives(self, ctx, obj):
        if obj.cache_cpath is None:
            obj.update()
        if obj.is_pixmap():
            self.render_image(ctx, obj)
            return
        if obj.is_text():
            if self.contour_flag:
                self.process_stroke(ctx, None, self.stroke_style)
                for item in obj.cache_cpath:
                    if item:
                        ctx.new_path()
                        ctx.append_path(item)
                        ctx.stroke()
            else:
                if obj.style[1] and obj.style[1][7]:
                    self.stroke_text_obj(ctx, obj)
                    self.fill_text_obj(ctx, obj)
                else:
                    self.fill_text_obj(ctx, obj)
                    self.stroke_text_obj(ctx, obj)
            return

        if self.contour_flag:
            ctx.new_path()
            self.process_stroke(ctx, None, self.stroke_style)
            ctx.append_path(obj.cache_cpath)
            ctx.stroke()
        else:
            if obj.style[1] and obj.style[1][7]:
                self.stroke_obj(ctx, obj)
                self.fill_obj(ctx, obj)
            else:
                self.fill_obj(ctx, obj)
                self.stroke_obj(ctx, obj)

    def fill_obj(self, ctx, obj):
        if obj.style[0]:
            ctx.new_path()
            self.process_fill(ctx, obj)
            ctx.append_path(obj.cache_cpath)
            ctx.fill()

    def fill_text_obj(self, ctx, obj):
        if obj.style[0]:
            self.process_fill(ctx, obj)
            for item in obj.cache_cpath:
                if item is not None:
                    ctx.new_path()
                    ctx.append_path(item)
                    ctx.fill()

    def stroke_obj(self, ctx, obj):
        if obj.style[1]:
            ctx.new_path()
            self.process_stroke(ctx, obj)
            ctx.append_path(obj.cache_cpath)
            ctx.stroke()

    def stroke_text_obj(self, ctx, obj):
        if obj.style[1]:
            self.process_stroke(ctx, obj)
            for item in obj.cache_cpath:
                if item is not None:
                    ctx.new_path()
                    ctx.append_path(item)
                    ctx.stroke()

    def process_fill(self, ctx, obj):
        fill = obj.style[0]
        fill_rule = fill[0]
        if fill_rule & sk2const.FILL_CLOSED_ONLY and not obj.is_closed():
            ctx.set_source_rgba(0.0, 0.0, 0.0, 0.0)
            return
        if fill_rule & sk2const.FILL_EVENODD:
            ctx.set_fill_rule(cairo.FILL_RULE_EVEN_ODD)
        else:
            ctx.set_fill_rule(cairo.FILL_RULE_WINDING)
        if fill[1] == sk2const.FILL_SOLID:
            if obj.fill_trafo:
                obj.fill_trafo = []
            color = fill[2]
            ctx.set_source_rgba(*self.get_color(color))
        elif fill[1] == sk2const.FILL_GRADIENT:
            if not obj.fill_trafo:
                obj.fill_trafo = [] + sk2const.NORMAL_TRAFO
            gradient = fill[2]
            points = gradient[1]
            if not points:
                obj.fill_trafo = [] + sk2const.NORMAL_TRAFO
                points = libgeom.bbox_middle_points(obj.cache_bbox)
                if gradient[0] == sk2const.GRADIENT_LINEAR:
                    points = [points[0], points[2]]
                else:
                    points = [[points[1][0], points[2][1]], points[2]]
                gradient[1] = points
            coords = points[0] + points[1]
            if gradient[0] == sk2const.GRADIENT_LINEAR:
                grd = cairo.LinearGradient(*coords)
            else:
                x0, y0 = coords[:2]
                radius = libgeom.distance(*points)
                grd = cairo.RadialGradient(x0, y0, 0, x0, y0, radius)
            for stop in gradient[2]:
                grd.add_color_stop_rgba(stop[0], *self.get_color(stop[1]))
            matrix = cairo.Matrix(*obj.fill_trafo)
            matrix.invert()
            extend = cairo.EXTEND_PAD
            if len(gradient) > 3:
                extend = EXTEND[gradient[3]]
            grd.set_extend(extend)
            grd.set_matrix(matrix)
            ctx.set_source(grd)
        elif fill[1] == sk2const.FILL_PATTERN:
            if not obj.fill_trafo:
                obj.fill_trafo = [] + sk2const.NORMAL_TRAFO
                obj.fill_trafo = obj.fill_trafo[:4] + [obj.cache_bbox[0],
                                                       obj.cache_bbox[3]]
            pattern_fill = fill[2]
            sp = cairo.SurfacePattern(self.get_pattern_image(obj))
            sp.set_extend(cairo.EXTEND_REPEAT)
            flip_matrix = cairo.Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)
            if len(pattern_fill) > 3:
                pattern_matrix = cairo.Matrix(*pattern_fill[3])
                pattern_matrix.invert()
                flip_matrix = flip_matrix * pattern_matrix
            trafo_matrix = cairo.Matrix(*obj.fill_trafo)
            trafo_matrix.invert()
            flip_matrix = flip_matrix * trafo_matrix
            sp.set_matrix(flip_matrix)
            ctx.set_source(sp)

            canvas_matrix = ctx.get_matrix()
            canvas_trafo = libcairo.get_trafo_from_matrix(canvas_matrix)
            zoom = canvas_trafo[0]
            if zoom * abs(obj.fill_trafo[0]) > .98:
                ctx.get_source().set_filter(cairo.FILTER_NEAREST)

    def process_stroke(self, ctx, obj, style=None):
        if style:
            stroke = style[1]
        else:
            stroke = obj.style[1]

        # Line width
        if not stroke[8]:
            line_width = stroke[1]
            if obj and obj.stroke_trafo:
                obj.stroke_trafo = []
        else:
            if obj and not obj.stroke_trafo:
                obj.stroke_trafo = [] + sk2const.NORMAL_TRAFO
            points = [[0.0, 0.0], [1.0, 0.0]]
            points = libgeom.apply_trafo_to_points(points, obj.stroke_trafo)
            coef = libgeom.distance(*points)
            line_width = stroke[1] * coef
        ctx.set_line_width(line_width)
        # Line color
        ctx.set_source_rgba(*self.get_color(stroke[2]))
        # Dashes
        dash = []
        for item in stroke[3]:
            dash.append(item * line_width)
        ctx.set_dash(dash)

        ctx.set_line_cap(CAPS[stroke[4]])
        ctx.set_line_join(JOINS[stroke[5]])
        ctx.set_miter_limit(stroke[6])
