'''
Color Picker
============

.. versionadded:: 1.7.0

.. warning::

    This widget is experimental. Its use and API can change at any time until
    this warning is removed.

.. image:: images/colorpicker.png
    :align: right

The ColorPicker widget allows a user to select a color from a chromatic
wheel where pinch and zoom can be used to change the wheel's saturation.
Sliders and TextInputs are also provided for entering the RGBA/HSV/HEX values
directly.

Usage::

    clr_picker = ColorPicker()
    parent.add_widget(clr_picker)

    # To monitor changes, we can bind to color property changes
    def on_color(instance, value):
        print("RGBA = ", str(value))  #  or instance.color
        print("HSV = ", str(instance.hsv))
        print("HEX = ", str(instance.hex_color))

    clr_picker.bind(color=on_color)


'''

__all__ = ('ColorPicker', 'ColorWheel')

from math import cos, sin, pi, sqrt, atan
from colorsys import rgb_to_hsv, hsv_to_rgb

from kivy.clock import Clock
from kivy.graphics import Mesh, InstructionGroup, Color
from kivy.logger import Logger
from kivy.properties import (NumericProperty, BoundedNumericProperty,
                             ListProperty, ObjectProperty,
                             ReferenceListProperty, StringProperty,
                             AliasProperty)
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex, get_hex_from_color


def distance(pt1, pt2):
    return sqrt((pt1[0] - pt2[0]) ** 2. + (pt1[1] - pt2[1]) ** 2.)


def polar_to_rect(origin, r, theta):
    return origin[0] + r * cos(theta), origin[1] + r * sin(theta)


def rect_to_polar(origin, x, y):
    if x == origin[0]:
        if y == origin[1]:
            return 0, 0
        elif y > origin[1]:
            return y - origin[1], pi / 2
        else:
            return origin[1] - y, 3 * pi / 2
    t = atan(float((y - origin[1])) / (x - origin[0]))
    if x - origin[0] < 0:
        t += pi

    if t < 0:
        t += 2 * pi

    return distance((x, y), origin), t


class ColorWheel(Widget):
    '''Chromatic wheel for the ColorPicker.

    .. versionchanged:: 1.7.1
        `font_size`, `font_name` and `foreground_color` have been removed. The
        sizing is now the same as others widget, based on 'sp'. Orientation is
        also automatically determined according to the width/height ratio.

    '''

    r = BoundedNumericProperty(0, min=0, max=1)
    '''The Red value of the color currently selected.

    :attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1. It defaults to 0.
    '''

    g = BoundedNumericProperty(0, min=0, max=1)
    '''The Green value of the color currently selected.

    :attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty`
    and can be a value from 0 to 1. It defaults to 0.
    '''

    b = BoundedNumericProperty(0, min=0, max=1)
    '''The Blue value of the color currently selected.

    :attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1. It defaults to 0.
    '''

    a = BoundedNumericProperty(0, min=0, max=1)
    '''The Alpha value of the color currently selected.

    :attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and
    can be a value from 0 to 1. It defaults to 0.
    '''

    color = ReferenceListProperty(r, g, b, a)
    '''The holds the color currently selected.

    :attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and
    contains a list of `r`, `g`, `b`, `a` values. It defaults to `[0, 0, 0, 0]`.
    '''

    _origin = ListProperty((100, 100))
    _radius = NumericProperty(100)

    _piece_divisions = NumericProperty(10)
    _pieces_of_pie = NumericProperty(16)

    _inertia_slowdown = 1.25
    _inertia_cutoff = .25

    _num_touches = 0
    _pinch_flag = False

    def __init__(self, **kwargs):
        self.arcs = []
        self.sv_idx = 0

        pdv = self._piece_divisions
        self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [
            (1, float(y) / pdv) for y in reversed(range(pdv))]

        super(ColorWheel, self).__init__(**kwargs)

    def on__origin(self, _instance, _value):
        self._reset_canvas()

    def on__radius(self, _instance, _value):
        self._reset_canvas()

    def _reset_canvas(self):
        # initialize list to hold all meshes
        self.canvas.clear()
        self.arcs = []
        self.sv_idx = 0
        pdv = self._piece_divisions
        ppie = self._pieces_of_pie

        for r in range(pdv):
            for t in range(ppie):
                self.arcs.append(
                    _ColorArc(
                        self._radius * (float(r) / float(pdv)),
                        self._radius * (float(r + 1) / float(pdv)),
                        2 * pi * (float(t) / float(ppie)),
                        2 * pi * (float(t + 1) / float(ppie)),
                        origin=self._origin,
                        color=(float(t) / ppie,
                               self.sv_s[self.sv_idx + r][0],
                               self.sv_s[self.sv_idx + r][1],
                               1)))

                self.canvas.add(self.arcs[-1])

    def recolor_wheel(self):
        ppie = self._pieces_of_pie
        for idx, segment in enumerate(self.arcs):
            segment.change_color(
                sv=self.sv_s[int(self.sv_idx + idx / ppie)])

    def change_alpha(self, val):
        for idx, segment in enumerate(self.arcs):
            segment.change_color(a=val)

    def inertial_incr_sv_idx(self, dt):
        # if its already zoomed all the way out, cancel the inertial zoom
        if self.sv_idx == len(self.sv_s) - self._piece_divisions:
            return False

        self.sv_idx += 1
        self.recolor_wheel()
        if dt * self._inertia_slowdown > self._inertia_cutoff:
            return False
        else:
            Clock.schedule_once(self.inertial_incr_sv_idx,
                                dt * self._inertia_slowdown)

    def inertial_decr_sv_idx(self, dt):
        # if its already zoomed all the way in, cancel the inertial zoom
        if self.sv_idx == 0:
            return False
        self.sv_idx -= 1
        self.recolor_wheel()
        if dt * self._inertia_slowdown > self._inertia_cutoff:
            return False
        else:
            Clock.schedule_once(self.inertial_decr_sv_idx,
                                dt * self._inertia_slowdown)

    def on_touch_down(self, touch):
        r = self._get_touch_r(touch.pos)
        if r > self._radius:
            return False

        # code is still set up to allow pinch to zoom, but this is
        # disabled for now since it was fiddly with small wheels.
        # Comment out these lines and  adjust on_touch_move to reenable
        # this.
        if self._num_touches != 0:
            return False

        touch.grab(self)
        self._num_touches += 1
        touch.ud['anchor_r'] = r
        touch.ud['orig_sv_idx'] = self.sv_idx
        touch.ud['orig_time'] = Clock.get_time()

    def on_touch_move(self, touch):
        if touch.grab_current is not self:
            return
        r = self._get_touch_r(touch.pos)
        goal_sv_idx = (touch.ud['orig_sv_idx'] -
                       int((r - touch.ud['anchor_r']) /
                            (float(self._radius) / self._piece_divisions)))

        if (
                goal_sv_idx != self.sv_idx and
                0 <= goal_sv_idx <= len(self.sv_s) - self._piece_divisions
        ):
            # this is a pinch to zoom
            self._pinch_flag = True
            self.sv_idx = goal_sv_idx
            self.recolor_wheel()

    def on_touch_up(self, touch):
        if touch.grab_current is not self:
            return
        touch.ungrab(self)
        self._num_touches -= 1
        if self._pinch_flag:
            if self._num_touches == 0:
                # user was pinching, and now both fingers are up. Return
                # to normal
                if self.sv_idx > touch.ud['orig_sv_idx']:
                    Clock.schedule_once(
                        self.inertial_incr_sv_idx,
                        (Clock.get_time() - touch.ud['orig_time']) /
                        (self.sv_idx - touch.ud['orig_sv_idx']))

                if self.sv_idx < touch.ud['orig_sv_idx']:
                    Clock.schedule_once(
                        self.inertial_decr_sv_idx,
                        (Clock.get_time() - touch.ud['orig_time']) /
                        (self.sv_idx - touch.ud['orig_sv_idx']))

                self._pinch_flag = False
                return
            else:
                # user was pinching, and at least one finger remains. We
                # don't want to treat the remaining fingers as touches
                return
        else:
            r, theta = rect_to_polar(self._origin, *touch.pos)
            # if touch up is outside the wheel, ignore
            if r >= self._radius:
                return
            # compute which ColorArc is being touched (they aren't
            # widgets so we don't get collide_point) and set
            # _hsv based on the selected ColorArc
            piece = int((theta / (2 * pi)) * self._pieces_of_pie)
            division = int((r / self._radius) * self._piece_divisions)
            hsva = list(
                self.arcs[self._pieces_of_pie * division + piece].color)
            self.color = list(hsv_to_rgb(*hsva[:3])) + hsva[-1:]

    def _get_touch_r(self, pos):
        return distance(pos, self._origin)


class _ColorArc(InstructionGroup):
    def __init__(self, r_min, r_max, theta_min, theta_max,
                 color=(0, 0, 1, 1), origin=(0, 0), **kwargs):
        super(_ColorArc, self).__init__(**kwargs)
        self.origin = origin
        self.r_min = r_min
        self.r_max = r_max
        self.theta_min = theta_min
        self.theta_max = theta_max
        self.color = color
        self.color_instr = Color(*color, mode='hsv')
        self.add(self.color_instr)
        self.mesh = self.get_mesh()
        self.add(self.mesh)

    def __str__(self):
        return "r_min: %s r_max: %s theta_min: %s theta_max: %s color: %s" % (
            self.r_min, self.r_max, self.theta_min, self.theta_max, self.color
        )

    def get_mesh(self):
        v = []
        # first calculate the distance between endpoints of the outer
        # arc, so we know how many steps to use when calculating
        # vertices
        theta_step_outer = 0.1
        theta = self.theta_max - self.theta_min
        d_outer = int(theta / theta_step_outer)
        theta_step_outer = theta / d_outer

        if self.r_min == 0:
            for x in range(0, d_outer, 2):
                v += (polar_to_rect(self.origin, self.r_max,
                                    self.theta_min + x * theta_step_outer
                                    ) * 2)
                v += polar_to_rect(self.origin, 0, 0) * 2
                v += (polar_to_rect(self.origin, self.r_max,
                                    self.theta_min + (x + 1) * theta_step_outer
                                    ) * 2)
            if not d_outer & 1:  # add a last point if d_outer is even
                v += (polar_to_rect(self.origin, self.r_max,
                                    self.theta_min + d_outer * theta_step_outer
                                    ) * 2)
        else:
            for x in range(d_outer + 1):
                v += (polar_to_rect(self.origin, self.r_min,
                                    self.theta_min + x * theta_step_outer
                                    ) * 2)
                v += (polar_to_rect(self.origin, self.r_max,
                                    self.theta_min + x * theta_step_outer
                                    ) * 2)

        return Mesh(vertices=v, indices=range(int(len(v) / 4)),
                    mode='triangle_strip')

    def change_color(self, color=None, color_delta=None, sv=None, a=None):
        self.remove(self.color_instr)
        if color is not None:
            self.color = color
        elif color_delta is not None:
            self.color = [self.color[i] + color_delta[i] for i in range(4)]
        elif sv is not None:
            self.color = (self.color[0], sv[0], sv[1], self.color[3])
        elif a is not None:
            self.color = (self.color[0], self.color[1], self.color[2], a)
        self.color_instr = Color(*self.color, mode='hsv')
        self.insert(0, self.color_instr)


class ColorPicker(RelativeLayout):
    '''
    See module documentation.
    '''

    font_name = StringProperty('data/fonts/RobotoMono-Regular.ttf')
    '''Specifies the font used on the ColorPicker.

    :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
    defaults to 'data/fonts/RobotoMono-Regular.ttf'.
    '''

    color = ListProperty((1, 1, 1, 1))
    '''The :attr:`color` holds the color currently selected in rgba format.

    :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to
    (1, 1, 1, 1).
    '''

    def _get_hsv(self):
        return rgb_to_hsv(*self.color[:3])

    def _set_hsv(self, value):
        if self._updating_clr:
            return
        self.set_color(value)

    hsv = AliasProperty(_get_hsv, _set_hsv, bind=('color', ))
    '''The :attr:`hsv` holds the color currently selected in hsv format.

    :attr:`hsv` is a :class:`~kivy.properties.ListProperty` and defaults to
    (1, 1, 1).
    '''
    def _get_hex(self):
        return get_hex_from_color(self.color)

    def _set_hex(self, value):
        if self._updating_clr:
            return
        self.set_color(get_color_from_hex(value)[:4])

    hex_color = AliasProperty(_get_hex, _set_hex, bind=('color',), cache=True)
    '''The :attr:`hex_color` holds the currently selected color in hex.

    :attr:`hex_color` is an :class:`~kivy.properties.AliasProperty` and
    defaults to `#ffffffff`.
    '''

    wheel = ObjectProperty(None)
    '''The :attr:`wheel` holds the color wheel.

    :attr:`wheel` is an :class:`~kivy.properties.ObjectProperty` and
    defaults to None.
    '''

    _update_clr_ev = _update_hex_ev = None

    # now used only internally.
    foreground_color = ListProperty((1, 1, 1, 1))

    def _trigger_update_clr(self, mode, clr_idx, text):
        if self._updating_clr:
            return
        self._updating_clr = True
        self._upd_clr_list = mode, clr_idx, text
        ev = self._update_clr_ev
        if ev is None:
            ev = self._update_clr_ev = Clock.create_trigger(self._update_clr)
        ev()

    def _update_clr(self, dt):
        # to prevent interaction between hsv/rgba, we work internally using rgba
        mode, clr_idx, text = self._upd_clr_list
        try:
            text = min(255.0, max(0.0, float(text)))
            if mode == 'rgb':
                self.color[clr_idx] = text / 255
            else:
                hsv = list(self.hsv[:])
                hsv[clr_idx] = text / 255
                self.color[:3] = hsv_to_rgb(*hsv)
        except ValueError:
            Logger.warning('ColorPicker: invalid value : {}'.format(text))
        finally:
            self._updating_clr = False

    def _update_hex(self, dt):
        try:
            if len(self._upd_hex_list) != 9:
                return
            self._updating_clr = False
            self.hex_color = self._upd_hex_list
        finally:
            self._updating_clr = False

    def _trigger_update_hex(self, text):
        if self._updating_clr:
            return
        self._updating_clr = True
        self._upd_hex_list = text
        ev = self._update_hex_ev
        if ev is None:
            ev = self._update_hex_ev = Clock.create_trigger(self._update_hex)
        ev()

    def set_color(self, color):
        self._updating_clr = True
        if len(color) == 3:
            self.color[:3] = color
        else:
            self.color = color
        self._updating_clr = False

    def __init__(self, **kwargs):
        self._updating_clr = False
        super(ColorPicker, self).__init__(**kwargs)


if __name__ in ('__android__', '__main__'):
    from kivy.app import App

    class ColorPickerApp(App):
        def build(self):
            cp = ColorPicker(pos_hint={'center_x': .5, 'center_y': .5},
                             size_hint=(1, 1))
            return cp
    ColorPickerApp().run()
