import math
from ableton.v2.base import task, listens
from ableton.v2.control_surface import MIDI_CC_TYPE, MIDI_NOTE_TYPE
from ableton.v2.control_surface.elements.button import ButtonElement as ButtonElementBase
from ableton.v2.control_surface.elements.encoder import EncoderElement
from ableton.v2.control_surface.elements.slider import SliderElement
from ableton.v2.control_surface.elements.sysex_element import SysexElement
from ableton.v2.control_surface.elements.button_matrix import ButtonMatrixElement
import Live

from .devmode import DevMode, devmode_command
from .util import normalized_parameter_value
from .color import Color, FX

"The MIDI CC numbers of each button on the Exquis"
BUTTON = {
    "settings": 100,
    "sound": 101,
    "record": 102,
    "loop": 103,
    "session": 104,
    "start_stop": 105,
    "down": 106,
    "up": 107,
    "left": 108,
    "right": 109
}

# can be overwritten by user in exquis app 
ENCODERBUTTON = {
    "encoder_push_0_CC": 21,
    "encoder_push_1_CC": 22,
    "encoder_push_2_CC": 23,
    "encoder_push_3_CC": 24
}

ENCODERBUTTONDEVMODE = {
	"encoder_push_0_CC_dev_mode": 114,
    "encoder_push_1_CC_dev_mode": 115,
    "encoder_push_2_CC_dev_mode": 116,
    "encoder_push_3_CC_dev_mode": 117
}

# can be overwritten by user in exquis app 
ENCODER = {
	"encoder_0_CC": 41,
    "encoder_1_CC": 42,
    "encoder_2_CC": 43,
    "encoder_3_CC": 44
}

ENCODERDEVMODE = {
	"encoder_0_CC_dev_mode": 110,
    "encoder_1_CC_dev_mode": 111,
    "encoder_2_CC_dev_mode": 112,
    "encoder_3_CC_dev_mode": 113
}

"The MIDI note/CC numbers corresponding to each pad of the Exquis, for each row"
MATRIX = [
    [55, 56, 57, 58, 59, 60],
    [50, 51, 52, 53, 54],
    [44, 45, 46, 47, 48, 49],
    [39, 40, 41, 42, 43],
    [33, 34, 35, 36, 37, 38],
    [28, 29, 30, 31, 32],
    [22, 23, 24, 25, 26, 27],
    [17, 18, 19, 20, 21],
    [11, 12, 13, 14, 15, 16],
    [6, 7, 8, 9, 10],
    [0, 1, 2, 3, 4, 5]
]

"The rows of the pad matrix that correspond to the clips"
MATRIX_CLIPS = [
    [56, 57, 58, 59],
    [50, 51, 52, 53],
    [45, 46, 47, 48],
    [39, 40, 41, 42],
    [34, 35, 36, 37],
    [28, 29, 30, 31],
    [23, 24, 25, 26],
]

MATRIX_SCENE_LAUNCH = [ 60, 54, 49, 43, 38, 32, 27 ]
MATRIX_STOP = [ 17, 18, 19, 20 ]
MATRIX_MUTE = [ 12, 13, 14, 15 ]
MATRIX_SOLO = [ 6, 7, 8, 9 ]
MATRIX_SELECT = [ 1, 2, 3, 4 ]
MATRIX_STOP_ALL_CLIPS = 21
MATRIX_SELECT_RIGHT = 5
MATRIX_SELECT_LEFT = 0
MATRIX_SOLO_RIGHT = 10
MATRIX_MUTE_RIGHT = 16
MATRIX_MUTE_LEFT = 11
MATRIX_ALL_BUT_LAST_ROW = MATRIX[:-1]

class ButtonElement(ButtonElementBase):
    pass

class UncoloredSliderElement(SliderElement):
    "A SliderElement that does not change its color"

    def send_value(self, value, force=False, channel=None):
        pass

class UncoloredButtonElement(ButtonElement):
    "A ButtonElement that does not change its color"

    def set_light(self, value):
        pass
    def send_value(self, value, force=False, channel=None):
        pass

class EncoderElementWithFeedback(EncoderElement):
    "An EncoderElement that changes its color based on the value of the parameter it is mapped to"

    def __init__(self, *a, **k):
        k["is_feedback_enabled"] = False
        super(EncoderElementWithFeedback, self).__init__(*a, **k)
        self._update_listener_task = self._tasks.add(task.run(self._update_parameter_listener))
        self._update_listener_task.kill()
        self.palette_index = 3
        self.is_bidirectional = False
    
    def disconnect(self):
        super(EncoderElementWithFeedback, self).disconnect()

    def install_connections(self, *a, **k):
        self._clear_parameter_listener()
        super(EncoderElementWithFeedback, self).install_connections(*a, **k)
        self._update_listener_task.restart()
    
    def connect_to(self, parameter):
        self._clear_parameter_listener()
        super(EncoderElementWithFeedback, self).connect_to(parameter)
        self._update_listener_task.restart()
    
    def release_parameter(self):
        super().release_parameter()
        self._update_listener_task.restart()

    def _update_parameter_listener(self):
        self._on_parameter_value_changed.subject = self._parameter_to_map_to
        self._on_parameter_value_changed()

    def _clear_parameter_listener(self):
        self._on_parameter_value_changed.subject = None

    @listens("value")
    def _on_parameter_value_changed(self):
        self._parameter_value_changed()
    
    def _parameter_value_changed(self):
        v = normalized_parameter_value(self._parameter_to_map_to)
        self.send_color(v)
    
    def send_color(self, v):
        fx = 0
        palette_index = self.palette_index
        if self.is_bidirectional:
            v = v * 2 - 1
            percent = int(math.fabs(v) * 100)
            if v > 0:
                if palette_index > 127:
                    palette_index = 5
                fx = FX.darker((100 - percent) * 0.99)
            elif v < 0:
                if palette_index > 127:
                    palette_index = 67
                fx = FX.darker((100 - percent) * 0.99)
            else:
                if palette_index > 127:
                    palette_index = 127
                fx = 0
        else:
            if palette_index > 127:
                palette_index = 3
            percent = int(v * 100)
            fx = FX.darker((100 - percent) * 0.99)
        
        Color(palette_index, fx).draw(self)

class TempoElement(SysexElement):
    "An Element that controls the tempo of the song"

    def __init__(self, *a, **k):
        super(TempoElement, self).__init__(
            sysex_identifier=devmode_command(DevMode.CMD_TEMPO)[0:-1],
            send_message_generator=lambda v: devmode_command(DevMode.CMD_TEMPO, (v >> 7) & 0x7F, v & 0x7F),
            enquire_message=devmode_command(DevMode.CMD_TEMPO),
            *a, **k)
    
    def receive_value(self, data):
        if len(data) != 2:
            return
        value = (data[0] << 7) | data[1]
        super(TempoElement, self).receive_value(value)

class RootNoteElement(SysexElement):
    "An Element that controls the root note of the scale"

    def __init__(self, *a, **k):
        super(RootNoteElement, self).__init__(
            sysex_identifier=devmode_command(DevMode.CMD_ROOT_NOTE)[0:-1],
            send_message_generator=lambda v: devmode_command(DevMode.CMD_ROOT_NOTE, v & 0x7F),
            enquire_message=devmode_command(DevMode.CMD_ROOT_NOTE),
            *a, **k)
    
    def receive_value(self, data):
        if len(data) != 1:
            return
        value = data[0]
        super(RootNoteElement, self).receive_value(value)

class ScaleIndexElement(SysexElement):
    "An Element that controls the index of the scale"

    def __init__(self, *a, **k):
        super(ScaleIndexElement, self).__init__(
            sysex_identifier=devmode_command(DevMode.CMD_SCALE_INDEX)[0:-1],
            send_message_generator=lambda v: devmode_command(DevMode.CMD_SCALE_INDEX, v & 0x7F),
            enquire_message=devmode_command(DevMode.CMD_SCALE_INDEX),
            *a, **k)
    
    def receive_value(self, data):
        if len(data) != 1:
            return
        value = data[0]
        super(ScaleIndexElement, self).receive_value(value)

def interval_array(value):
    "Return an array of 12 booleans representing the intervals of the given scale"

    result = [False] * 12
    for i in value:
        if i < 12:
            result[i] = True
    return result

class CustomScaleElement(SysexElement):
    "An Element that controls the index of the custom scale"

    def __init__(self, *a, **k):
        super(CustomScaleElement, self).__init__(
            sysex_identifier=devmode_command(DevMode.CMD_SCALE_CUSTOM)[0:-1],
            send_message_generator=lambda v: devmode_command(DevMode.CMD_SCALE_CUSTOM, *interval_array(v)),
            *a, **k)
    
    def receive_value(self, data):
        if len(data) != 12:
            return
        value = []
        for i in range(0, 12):
            if data[i]:
                value.append(i)
        super(CustomScaleElement, self).receive_value(value)


def button_matrix(l):
    "Return a ButtonMatrixElement with the given list of buttons"

    m = ButtonMatrixElement()
    for e in l:
        m.add_row([e])
    return m

class Elements:
    "An object that contains all the Elements available on the Exquis and that will be used by the ControlSurface"

    def __init__(self, skin):
        
        # Buttons 
        self.button = {}
        for k in BUTTON.keys():
            cls = ButtonElement
            b = cls(True, MIDI_CC_TYPE, 15, BUTTON[k], skin=skin, name=k)
            self.button[k] = b

        self.encoders_buttons = {}
        for k in ENCODERBUTTON.keys():
            cls = UncoloredButtonElement
            b = cls(True, MIDI_CC_TYPE, 0, ENCODERBUTTON[k], skin=skin, name=k)
            self.encoders_buttons[k] = b
        self.encoders_buttons_elements = button_matrix([self.encoders_buttons["encoder_push_" + str(i) + "_CC"] for i in range(4)])

        self.encoders_buttons_dev_mode = {}
        for k in ENCODERBUTTONDEVMODE.keys():
            cls = UncoloredButtonElement
            b = cls(True, MIDI_CC_TYPE, 15, ENCODERBUTTONDEVMODE[k], skin=skin, name=k)
            self.encoders_buttons_dev_mode[k] = b
        self.encoders_buttons_dev_mode_elements = button_matrix([self.encoders_buttons_dev_mode["encoder_push_" + str(i) + "_CC_dev_mode"] for i in range(4)])
        
        # Entire pad matrix
        matrix = {}
        self.matrix = ButtonMatrixElement()
        self.matrix_all = ButtonMatrixElement()
        for row in MATRIX:
            r = []
            for i in row:
                b = ButtonElement(True, MIDI_NOTE_TYPE, 15, i, skin=skin, name="Pad" + str(i))
                matrix[i] = b
                r.append(b)
                self.matrix_all.add_row([b])
            self.matrix.add_row(r)
        
        # Submatrices
        def submatrix(rows):
            m = ButtonMatrixElement()
            for row in rows:
                r = []
                for i in row:
                    r.append(matrix[i])
                m.add_row(r)
            return m
        def submatrix_oneline(rows):
            m = ButtonMatrixElement()
            for row in rows:
                for i in row:
                    m.add_row([matrix[i]])
            return m
        
        self.matrix_clips = submatrix(MATRIX_CLIPS)
        self.matrix_all_but_last_row = submatrix_oneline(MATRIX_ALL_BUT_LAST_ROW)

        # Other one-line submatrices
        def oneline_submatrix(row):
            m = ButtonMatrixElement()
            for i in row:
                b = matrix[i]
                m.add_row([b])
            return m
        self.matrix_scene_launch = oneline_submatrix(MATRIX_SCENE_LAUNCH)
        self.matrix_solo = oneline_submatrix(MATRIX_SOLO)
        self.matrix_mute = oneline_submatrix(MATRIX_MUTE)
        self.matrix_select = oneline_submatrix(MATRIX_SELECT)
        self.matrix_stop = oneline_submatrix(MATRIX_STOP)
        self.matrix_last_row = oneline_submatrix(MATRIX[-1])
        self.matrix_stop_all_clips = matrix[MATRIX_STOP_ALL_CLIPS]
        self.matrix_select_left = matrix[MATRIX_SELECT_LEFT]
        self.matrix_select_right = matrix[MATRIX_SELECT_RIGHT]
        self.matrix_solo_right = matrix[MATRIX_SOLO_RIGHT]
        self.matrix_mute_left = matrix[MATRIX_MUTE_LEFT]
        self.matrix_mute_right = matrix[MATRIX_MUTE_RIGHT]
        
        # Encoders Dev Mode
        self.encoders_dev_mode = []
        for i in range(0, 4):
            e = EncoderElementWithFeedback(
                MIDI_CC_TYPE,
                15,
                110 + i,
                map_mode=Live.MidiMap.MapMode.relative_smooth_binary_offset,
                name="EncoderDevMode" + str(i),
            )
            self.encoders_dev_mode.append(e)
        self.encoders_dev_mode_elements = ButtonMatrixElement(rows=[self.encoders_dev_mode], name="EncodersDevMode")

        # Standalone encoders
        self.encoders = []
        for i in range(0, 4):
            e = EncoderElement(
                MIDI_CC_TYPE,
                0,
                41 + i,
                map_mode=Live.MidiMap.MapMode.absolute,
                name="Encoder" + str(i),
            )
            self.encoders.append(e)
        self.encoders_elements = ButtonMatrixElement(rows=[self.encoders], name="Encoders")

        # Slider
        self.slider = ButtonMatrixElement()
        for i in range(0, 6):
            s = ButtonElement(True, MIDI_CC_TYPE, 15, 80 + i, name="SliderPart" + str(i), skin=skin)
            self.slider.add_row([s])
        
        # Other MIDI signals
        self.refresh = SysexElement(
            sysex_identifier=devmode_command(DevMode.CMD_REFRESH)[0:-1],
            name="Refresh"
        )
        self.track_snapshot = SysexElement(
            sysex_identifier=devmode_command(DevMode.CMD_SNAPSHOT)[0:-1],
            name="TrackSnapshot"
        )
        self.encoders_ccs_snapshot = SysexElement(
            sysex_identifier=devmode_command(DevMode.CMD_ENCODERS_CCS_SNAPSHOT)[0:-1],
            name="EncoderCCSnapshot"
        )
        self.tempo = TempoElement(name="Tempo")
        self.tempo_push = ButtonElement(True, MIDI_CC_TYPE, 15, 118, name="SettingsTempoPush")
        self.root_note = RootNoteElement(name="RootNote")
        self.scale_index = ScaleIndexElement(name="ScaleIndex")
        self.custom_scale = CustomScaleElement(name="CustomScale")

    def update_encoders_cc(self, ccs, skin):
        """
        Updates encoder and encoder button elements using the given list of 8 CC numbers.
        First 4 are encoders, last 4 are encoder push buttons.
        """
        # --- Encoders ---
        self.encoders = []
        for i, cc in enumerate(ccs[:4]):
            e = EncoderElement(
                MIDI_CC_TYPE,
                0,
                cc,
                map_mode=Live.MidiMap.MapMode.absolute,
                name="Encoder" + str(i),
            )
            self.encoders.append(e)
        self.encoders_elements = ButtonMatrixElement(rows=[self.encoders], name="Encoders")

        # --- Encoder push buttons ---
        self.encoders_buttons = {}
        for i, cc in enumerate(ccs[4:8]):
            key = "encoder_push_" + str(i) + "_CC"
            b = UncoloredButtonElement(True, MIDI_CC_TYPE, 0, cc, skin=skin, name=key)
            self.encoders_buttons[key] = b
        self.encoders_buttons_elements = button_matrix(
            [self.encoders_buttons["encoder_push_" + str(i) + "_CC"] for i in range(4)]
        )