# Copyright (c) 2007-8, Playful Invention Company.
# Copyright (c) 2008-11, Walter Bender
# Copyright (c) 2011 Collabora Ltd. <http://www.collabora.co.uk/>

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import cairo
import os

from math import pi

from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import Pango
from gi.repository import PangoCairo
from gi.repository import GObject
from tautils import get_path
from taconstants import (Color, TMP_SVG_PATH, DEFAULT_PEN_COLOR,
                         DEFAULT_BACKGROUND_COLOR, DEFAULT_FONT)


def wrap100(n):
    ''' A variant on mod... 101 -> 99; 199 -> 1 '''
    n = int(n)
    n %= 200
    if n > 99:
        n = 199 - n
    return n


def calc_shade(c, s, invert=False):
    ''' Convert a color to the current shade (lightness/darkness). '''
    # Assumes 16 bit input values
    if invert:
        if s == -1:
            return int(c)
        elif s < 0:
            return int(c / (1 + s))
        return int((c - 65536 * s) / (1 - s))
    else:
        if s < 0:
            return int(c * (1 + s))
        return int(c + (65536 - c) * s)


def calc_gray(c, g, invert=False):
    ''' Gray is a psuedo saturation calculation. '''
    # Assumes 16 bit input values
    if g == 100:
        return int(c)
    if invert:
        if g == 0:
            return int(c)
        else:
            return int(((c * 100) - (32768 * (100 - g))) / g)
    else:
        return int(((c * g) + (32768 * (100 - g))) / 100)


colors = {}
DEGTOR = pi / 180.
RTODEG = 180. / pi

COLOR_TABLE = (
    0xFF0000, 0xFF0D00, 0xFF1A00, 0xFF2600, 0xFF3300,
    0xFF4000, 0xFF4D00, 0xFF5900, 0xFF6600, 0xFF7300,
    0xFF8000, 0xFF8C00, 0xFF9900, 0xFFA600, 0xFFB300,
    0xFFBF00, 0xFFCC00, 0xFFD900, 0xFFE600, 0xFFF200,
    0xFFFF00, 0xE6FF00, 0xCCFF00, 0xB3FF00, 0x99FF00,
    0x80FF00, 0x66FF00, 0x4DFF00, 0x33FF00, 0x1AFF00,
    0x00FF00, 0x00FF0D, 0x00FF1A, 0x00FF26, 0x00FF33,
    0x00FF40, 0x00FF4D, 0x00FF59, 0x00FF66, 0x00FF73,
    0x00FF80, 0x00FF8C, 0x00FF99, 0x00FFA6, 0x00FFB3,
    0x00FFBF, 0x00FFCC, 0x00FFD9, 0x00FFE6, 0x00FFF2,
    0x00FFFF, 0x00F2FF, 0x00E6FF, 0x00D9FF, 0x00CCFF,
    0x00BFFF, 0x00B3FF, 0x00A6FF, 0x0099FF, 0x008CFF,
    0x0080FF, 0x0073FF, 0x0066FF, 0x0059FF, 0x004DFF,
    0x0040FF, 0x0033FF, 0x0026FF, 0x001AFF, 0x000DFF,
    0x0000FF, 0x0D00FF, 0x1A00FF, 0x2600FF, 0x3300FF,
    0x4000FF, 0x4D00FF, 0x5900FF, 0x6600FF, 0x7300FF,
    0x8000FF, 0x8C00FF, 0x9900FF, 0xA600FF, 0xB300FF,
    0xBF00FF, 0xCC00FF, 0xD900FF, 0xE600FF, 0xF200FF,
    0xFF00FF, 0xFF00E6, 0xFF00CC, 0xFF00B3, 0xFF0099,
    0xFF0080, 0xFF0066, 0xFF004D, 0xFF0033, 0xFF001A)


class TurtleGraphics:

    ''' A class for the Turtle graphics canvas '''

    def __init__(self, turtle_window, width, height):
        ''' Create a sprite to hold the canvas. '''
        self.turtle_window = turtle_window
        self.width = width
        self.height = height
        self.textsize = 48
        self._fgrgb = DEFAULT_PEN_COLOR
        self._bgrgb = DEFAULT_BACKGROUND_COLOR
        self._font = DEFAULT_FONT
        self._shade = 0
        self._color = 0
        self._gray = 100
        self.cr_svg = None  # Surface used for saving to SVG

        # Build a cairo.Context from a cairo.XlibSurface
        self.canvas = cairo.Context(self.turtle_window.turtle_canvas)
        self.set_pen_size(5)

    def setup_svg_surface(self):
        ''' Set up a surface for saving to SVG '''
        svg_surface = cairo.SVGSurface(self.get_svg_path(),
                                       self.width, self.height)
        self.svg_surface = svg_surface
        self.cr_svg = cairo.Context(svg_surface)
        self.cr_svg.set_line_cap(1)  # Set the line cap to be round

    def get_svg_path(self):
        '''We use a separate file for the svg used for generating Sugar icons
        '''
        if self.turtle_window.running_sugar:
            return os.path.join(get_path(self.turtle_window.activity,
                                         'instance'), 'output.svg')
        else:
            return TMP_SVG_PATH

    def fill_polygon(self, poly_points):
        ''' Draw the polygon... '''
        def _fill_polygon(cr, poly_points):
            cr.new_path()
            for i, p in enumerate(poly_points):
                if p[0] == 'move':
                    if i == len(poly_points) - 1 or \
                       poly_points[i + 1][0] not in ['rarc', 'larc']:
                        cr.move_to(p[1], p[2])
                elif p[0] == 'rarc':
                    cr.arc(p[1], p[2], p[3], p[4], p[5])
                elif p[0] == 'larc':
                    cr.arc_negative(p[1], p[2], p[3], p[4], p[5])
                else:  # line
                    cr.line_to(p[1], p[2])
            cr.close_path()
            cr.fill()

        _fill_polygon(self.canvas, poly_points)
        self.inval()
        if self.cr_svg is not None:
            _fill_polygon(self.cr_svg, poly_points)

    def clearscreen(self):
        '''Clear the canvas and reset most graphics attributes to defaults.'''

        def _clearscreen(cr):
            cr.move_to(0, 0)
            self._bgrgb = DEFAULT_BACKGROUND_COLOR
            cr.set_source_rgb(self._bgrgb[0] / 255.,
                              self._bgrgb[1] / 255.,
                              self._bgrgb[2] / 255.)
            cr.rectangle(0, 0, self.width * 2, self.height * 2)
            cr.fill()

        _clearscreen(self.canvas)
        self.inval()
        if self.cr_svg is not None:
            _clearscreen(self.cr_svg)

    def rarc(self, x, y, r, a, heading):
        ''' draw a clockwise arc '''
        def _rarc(cr, x, y, r, a, h):
            cr.arc(x, y, r, (h - 180) * DEGTOR, (h - 180 + a) * DEGTOR)
            cr.stroke()

        _rarc(self.canvas, x, y, r, a, heading)
        self.inval()

        if self.cr_svg is not None:
            _rarc(self.cr_svg, x, y, r, a, heading)

    def larc(self, x, y, r, a, heading):
        ''' draw a counter-clockwise arc '''
        def _larc(cr, x, y, r, a, h):
            cr.arc_negative(x, y, r, h * DEGTOR, (h - a) * DEGTOR)
            cr.stroke()

        _larc(self.canvas, x, y, r, a, heading)
        self.inval()
        if self.cr_svg is not None:
            _larc(self.cr_svg, x, y, r, a, heading)

    def set_pen_size(self, pen_size):
        ''' Set the pen size '''
        self.canvas.set_line_width(pen_size)
        if self.cr_svg is not None:
            self.cr_svg.set_line_width(pen_size)

    def fillscreen(self, c, s):
        ''' Deprecated method: Fill screen with color/shade '''
        self.fillscreen_with_gray(c, s, self._gray)

    def fillscreen_with_gray(self, color, shade, gray):
        ''' Fill screen with color/shade/gray and reset to defaults '''

        save_rgb = self._fgrgb[:]

        # Special case for color blocks
        if isinstance(color, Color):
            if color.color is None:
                self._shade = color.shade
            else:
                self._color = color.color
        else:
            self._color = color
        if isinstance(shade, Color):
            self._shade = shade.shade
        else:
            self._shade = shade
        if isinstance(gray, Color):
            self._gray = gray.gray
        else:
            self._gray = gray

        if self._gray < 0:
            self._gray = 0
        if self._gray > 100:
            self._gray = 100

        self.set_fgcolor(shade=self._shade, gray=self._gray, color=self._color)
        self._bgrgb = self._fgrgb[:]

        def _fillscreen(cr, rgb, w, h):
            cr.set_source_rgb(rgb[0] / 255., rgb[1] / 255., rgb[2] / 255.)
            cr.rectangle(0, 0, w * 2, h * 2)
            cr.fill()

        _fillscreen(self.canvas, self._fgrgb, self.width, self.height)
        self.inval()
        if self.cr_svg is not None:
            _fillscreen(self.cr_svg, self._fgrgb, self.width, self.height)

        self._fgrgb = save_rgb[:]

    def set_fgcolor(self, shade=None, gray=None, color=None):
        ''' Set the foreground color '''
        if shade is not None:
            self._shade = shade
        if gray is not None:
            self._gray = gray
        if color is not None:
            self._color = color
        sh = (wrap100(self._shade) - 50) / 50.0
        rgb = COLOR_TABLE[wrap100(self._color)]
        r = (rgb >> 8) & 0xff00
        r = calc_gray(r, self._gray)
        r = calc_shade(r, sh)
        g = rgb & 0xff00
        g = calc_gray(g, self._gray)
        g = calc_shade(g, sh)
        b = (rgb << 8) & 0xff00
        b = calc_gray(b, self._gray)
        b = calc_shade(b, sh)
        self._fgrgb = [r >> 8, g >> 8, b >> 8]

    def draw_surface(self, surface, x, y, w, h):
        ''' Draw a surface '''

        def _draw_surface(cc, surface, x, y, w, h):
            cc.set_source_surface(surface, x, y)
            cc.rectangle(x, y, w, h)
            cc.fill()

        _draw_surface(self.canvas, surface, x, y, w, h)
        self.inval()
        if self.cr_svg is not None:
            _draw_surface(self.cr_svg, surface, x, y, w, h)

    def draw_pixbuf(self, pixbuf, a, b, x, y, w, h, heading):
        ''' Draw a pixbuf '''

        def _draw_pixbuf(cc, pixbuf, a, b, x, y, w, h, heading):
            # Build a gtk.gdk.CairoContext from a cairo.Context to access
            # the set_source_pixbuf attribute.
            cc.save()
            # center the rotation on the center of the image
            cc.translate(x + w / 2., y + h / 2.)
            cc.rotate(heading * DEGTOR)
            cc.translate(-x - w / 2., -y - h / 2.)
            Gdk.cairo_set_source_pixbuf(cc, pixbuf, x, y)
            cc.rectangle(x, y, w, h)
            cc.fill()
            cc.restore()

        _draw_pixbuf(self.canvas, pixbuf, a, b, x, y, w, h, heading)
        self.inval()
        if self.cr_svg is not None:
            _draw_pixbuf(self.cr_svg, pixbuf, a, b, x, y, w, h, heading)

    def set_font(self, font_name):
        ''' Set font used by draw_text '''
        self._font = str(font_name)

    def draw_text(self, label, x, y, size, width, heading, scale):
        ''' Draw text '''

        def _draw_text(cc, label, x, y, size, width, scale, heading, rgb,
                       wrap=False):
            import textwrap

            final_scale = int(size * scale) * Pango.SCALE
            label = str(label)
            if wrap:
                label = '\n'.join(textwrap.wrap(label, int(width / scale)))

            pl = PangoCairo.create_layout(cc)
            fd = Pango.FontDescription(self._font)
            fd.set_size(final_scale)
            pl.set_font_description(fd)
            if isinstance(label, (str, unicode)):
                text = label.replace('\0', ' ')
            elif isinstance(label, (float, int)):
                text = str(label)
            else:
                text = label

            pl.set_text(str(label), len(str(label)))
            pl.set_width(int(width) * Pango.SCALE)
            cc.save()
            cc.translate(x, y)
            cc.rotate(heading * DEGTOR)
            cc.set_source_rgb(rgb[0] / 255., rgb[1] / 255., rgb[2] / 255.)
            PangoCairo.update_layout(cc, pl)
            PangoCairo.show_layout(cc, pl)
            cc.restore()

        width *= scale
        _draw_text(self.canvas, label, x, y, size, width, scale, heading,
                   self._fgrgb)
        self.inval()
        if self.cr_svg is not None:  # and self.pendown:
            _draw_text(self.cr_svg, label, x, y, size, width, scale, heading,
                       self._fgrgb, wrap=True)

    def set_source_rgb(self):
        r = self._fgrgb[0] / 255.
        g = self._fgrgb[1] / 255.
        b = self._fgrgb[2] / 255.
        self.canvas.set_source_rgb(r, g, b)
        if self.cr_svg is not None:
            self.cr_svg.set_source_rgb(r, g, b)

    def draw_line(self, x1, y1, x2, y2):
        ''' Draw a line '''

        def _draw_line(cr, x1, y1, x2, y2):
            cr.set_line_cap(1)  # Set the line cap to be round
            cr.move_to(x1, y1)
            cr.line_to(x2, y2)
            cr.stroke()

        _draw_line(self.canvas, x1, y1, x2, y2)
        if self.cr_svg is not None:
            _draw_line(self.cr_svg, x1, y1, x2, y2)
        self.inval()

    def get_color_index(self, r, g, b, a=0):
        ''' Find the closest palette entry to the rgb triplet '''
        if self._shade != 50 or self._gray != 100:
            r <<= 8
            g <<= 8
            b <<= 8
            if self._shade != 50:
                sh = (wrap100(self._shade) - 50) / 50.
                r = calc_shade(r, sh, True)
                g = calc_shade(g, sh, True)
                b = calc_shade(b, sh, True)
            if self._gray != 100:
                r = calc_gray(r, self._gray, True)
                g = calc_gray(g, self._gray, True)
                b = calc_gray(b, self._gray, True)
            r >>= 8
            g >>= 8
            b >>= 8
        min_distance = 1000000
        closest_color = -1
        for i, c in enumerate(COLOR_TABLE):
            cr = int((c & 0xff0000) >> 16)
            cg = int((c & 0x00ff00) >> 8)
            cb = int((c & 0x0000ff))
            distance_squared = \
                ((cr - r) ** 2) + ((cg - g) ** 2) + ((cb - b) ** 2)
            if distance_squared == 0:
                return i
            if distance_squared < min_distance:
                min_distance = distance_squared
                closest_color = i
        return closest_color

    def get_pixel(self, x, y):
        ''' Read the pixel at x, y '''
        if self.turtle_window.interactive_mode:
            x = int(x)
            y = int(y)
            w = self.turtle_window.turtle_canvas.get_width()
            h = self.turtle_window.turtle_canvas.get_height()
            if x < 0 or x > (w - 1) or y < 0 or y > (h - 1):
                return(-1, -1, -1, -1)
            # create a new 1x1 cairo surface
            cs = cairo.ImageSurface(cairo.FORMAT_RGB24, 1, 1)
            cr = cairo.Context(cs)
            cr.set_source_surface(self.turtle_window.turtle_canvas, -x, -y)
            cr.rectangle(0, 0, 1, 1)
            cr.set_operator(cairo.OPERATOR_SOURCE)
            cr.fill()
            cs.flush()  # ensure all writing is done
            pixels = cs.get_data()  # Read the pixel
            return (ord(pixels[2]), ord(pixels[1]), ord(pixels[0]), 0)
        else:
            return(-1, -1, -1, -1)

    def svg_close(self):
        ''' Close current SVG graphic '''
        self.cr_svg.show_page()
        self.svg_surface.flush()
        self.svg_surface.finish()

    def svg_reset(self):
        ''' Reset svg flags '''
        self.cr_svg = None

    def inval(self):
        ''' Invalidate a region for gtk '''
        self.turtle_window.inval_all()
