# This file is part of roccat-tools.
#
# roccat-tools 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 2 of the License, or
# (at your option) any later version.
#
# roccat-tools 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 roccat-tools. If not, see <http://www.gnu.org/licenses/>.

import signal
import ryos

# Implements ripple effect
# Differences to Roccat implementation
# Roccat uses a vague position table and costly floating point arithmetics for
# drawing a circle.
# I'm using a fine grain position table and a cheap integer algorithm for drawing.
class Ripple():
    __MATRIX_ROWS = 30
    __MATRIX_COLS = 95
    __VOID = 0xff
    __KEY_WIDTH = 4

    # Dictionary to map hid codes to center positions on a positional matrix.
    # These positions are not correct for all keyboard layouts, but that shouldn't
    # impair pleasure.
    # Format: hid : (row, col)
    light_positions = {
        # thumbster keys don't send events
        
        # keys_macro
        0xaa : ( 7,  2), 0xab : (11,  2), 0xac : (15,  2), 0xad : (19,  2),
        0xae : (23,  2),

        # keys_function
        0x3a : ( 2, 15), 0x3b : ( 2, 19), 0x3c : ( 2, 23), 0x3d : ( 2, 27),
        0x3e : ( 2, 33), 0x3f : ( 2, 37), 0x40 : ( 2, 41), 0x41 : ( 2, 45),
        0x42 : ( 2, 51), 0x43 : ( 2, 55), 0x44 : ( 2, 59), 0x45 : ( 2, 63),
        0x46 : ( 2, 68), 0x47 : ( 2, 72), 0x48 : ( 2, 76),

        # keys_easyzone
        0x35 : ( 7,  7), 0x1e : ( 7, 11), 0x1f : ( 7, 15), 0x20 : ( 7, 19),
        0x21 : ( 7, 23), 0x22 : ( 7, 27), 0x23 : ( 7, 31), 0x24 : ( 7, 35),
        0x25 : ( 7, 39), 0x26 : ( 7, 43), 0x27 : ( 7, 47), 0x2d : ( 7, 51),
        0x2e : ( 7, 55), 0x89 : ( 7, 59), 0x2a : ( 7, 63), 0x49 : ( 7, 68),
        0x4a : ( 7, 72), 0x4b : ( 7, 76), 0x53 : ( 7, 81), 0x54 : ( 7, 85),
        0x55 : ( 7, 89), 0x56 : ( 7, 93), 0x2b : (11,  8), 0x14 : (11, 13),
        0x1a : (11, 17), 0x08 : (11, 21), 0x15 : (11, 25), 0x17 : (11, 29),
        0x1c : (11, 33), 0x18 : (11, 37), 0x0c : (11, 41), 0x12 : (11, 45),
        0x13 : (11, 49), 0x2f : (11, 53), 0x30 : (11, 57), 0x31 : (11, 62),
        0x4c : (11, 68), 0x4d : (11, 72), 0x4e : (11, 76), 0x5f : (11, 81),
        0x60 : (11, 85), 0x61 : (11, 89), 0x57 : (13, 93), 0x04 : (15, 14),
        0x16 : (15, 18), 0x07 : (15, 22), 0x09 : (15, 26), 0x0a : (15, 30),
        0x0b : (15, 34), 0x0d : (15, 38), 0x0e : (15, 42), 0x0f : (15, 46),
        0x33 : (15, 50), 0x34 : (15, 54), 0x32 : (15, 58), 0x28 : (15, 62),
        0x5c : (15, 81), 0x5d : (15, 85), 0x5e : (15, 89), 0xe1 : (19,  8),
        0x64 : (19, 12), 0x1d : (19, 16), 0x1b : (19, 20), 0x06 : (19, 24),
        0x19 : (19, 28), 0x05 : (19, 32), 0x11 : (19, 36), 0x10 : (19, 40),
        0x36 : (19, 44), 0x37 : (19, 48), 0x38 : (19, 52), 0x87 : (19, 56),
        0xe5 : (19, 61), 0x52 : (19, 72), 0x59 : (19, 81), 0x5a : (19, 85),
        0x5b : (19, 89), 0x58 : (21, 93), 0xe0 : (23,  8), 0xe3 : (23, 13),
        0xe2 : (23, 18), 0x8b : (23, 22), 0x2c : (23, 31), 0x8a : (23, 40),
        0x88 : (23, 44), 0xe6 : (23, 49), 0x65 : (23, 57), 0xe4 : (23, 62),
        0x50 : (23, 68), 0x51 : (23, 72), 0x4f : (23, 76), 0x62 : (23, 83),
        0x63 : (23, 89),

        # keys_extra
        0x39 : (15,  9), 0xf1 : (23, 53), 0x29 : ( 2,  7),
    }

    def __init__(self):
        self.__prepare_position_matrix()

    def __prepare_position_matrix(self):
        self.position_matrix = [[self.__VOID for x in xrange(self.__MATRIX_COLS)] for x in xrange(self.__MATRIX_ROWS)]
        for hid, (row, col) in self.light_positions.iteritems():
            self.position_matrix[row-1][col-2] = hid
            self.position_matrix[row][col-2] = hid
            self.position_matrix[row-2][col-1] = hid
            self.position_matrix[row-1][col-1] = hid
            self.position_matrix[row][col-1] = hid
            self.position_matrix[row+1][col-1] = hid
            self.position_matrix[row-2][col] = hid
            self.position_matrix[row-1][col] = hid
            self.position_matrix[row][col] = hid
            self.position_matrix[row+1][col] = hid
            self.position_matrix[row-1][col+1] = hid
            self.position_matrix[row][col+1] = hid
    
    # Returns true if position is in range
    def __set_led(self, row, col, value):
        if row >= self.__MATRIX_ROWS or row < 0 or col >= self.__MATRIX_COLS or col < 0:
            return False
    
        hid = self.position_matrix[row][col]
        if hid == self.__VOID:
            return True
        
        if value:
            ryos.set_key(hid)
        else:
            ryos.clear_key(hid)
    
        return True

    # QUOTE Midpoint circle algorithm inspired by Perone's programming pad
    # http://willperone.net/Code/codecircle.php
    # Copyright (c) Will Perone
    #
    # retval True if at least one keyboard position, False if break
    def __draw_circle(self, center_row, center_col, radius, value):
        radius *= self.__KEY_WIDTH
    
        if radius < 1:
            return True
        
        go_on = False
    
        x = 0
        y = radius
        d = 1 - radius
        delta_e = 3
        delta_se = 5 - radius * 2
        
        if self.__set_led(center_row - y, center_col, value): go_on = True
        if self.__set_led(center_row + y, center_col, value): go_on = True
        if self.__set_led(center_row, center_col - y, value): go_on = True
        if self.__set_led(center_row, center_col + y, value): go_on = True
        
        while y > x:
            if d < 0:
                d += delta_e
                delta_se += 2
            else:
                d += delta_se
                delta_se += 4
                y -= 1
            delta_e += 2
            x += 1
    
            if self.__set_led(center_row - y, center_col - x, value): go_on = True
            if self.__set_led(center_row - x, center_col - y, value): go_on = True
            if self.__set_led(center_row - x, center_col + y, value): go_on = True
            if self.__set_led(center_row - y, center_col + x, value): go_on = True
            if self.__set_led(center_row + y, center_col - x, value): go_on = True
            if self.__set_led(center_row + x, center_col - y, value): go_on = True
            if self.__set_led(center_row + x, center_col + y, value): go_on = True
            if self.__set_led(center_row + y, center_col + x, value): go_on = True
    
        return go_on

    # One tick: draw new outer circle
    # Next tick: remove inner circle
    # Next tick: draw new outer circle
    def tick(self):
        if self.remove:
            if not self.__draw_circle(self.row, self.col, self.radius - 1, False):
                return None
            self.radius += 1
            self.remove = not self.remove
            
            if ryos.empty():
                return None
            
            return 0
        else:
            if not self.__draw_circle(self.row, self.col, self.radius, True):
                return None
            self.remove = not self.remove
            return 50 # msec

    def start(self, hid):
        self.row, self.col = self.light_positions[hid]
        self.radius = 1
        self.remove = False

# Execution starts here on load

# Set default handler for SIGINT. This way the eventhandler can be killed with Ctrl-C
signal.signal(signal.SIGINT, signal.SIG_DFL)

# Create instance of class and let callbacks interact with instance
instance = Ripple()

def Start(hid):
    instance.start(hid)
    
# Should return waiting time in msec until next tick or None
def Tick():
    return instance.tick()
