import pytest
from mido import Message

from solo_tool.midi_controller_launchpad_mini import MidiController
from solo_tool.solo_tool import SoloTool
from player_mock import Player as PlayerMock

LED_RED = 3
LED_YELLOW = 126
LED_GREEN = 124
LED_OFF = 0

nextSongButton = 55
fwd25PcButton = 54
fwd5PcButton = 53
fwd1PcButton = 52
rwd1PcButton = 51
rwd5PcButton = 50
rwd25PcButton = 49
previousSongButton = 48

playPauseButton = 112
stopButton = 96

nextKeyPositionButton = 119
previousKeyPositionButton = 118
setKeyPositionButton = 117
jumpToKeyPositionButton = 114

class MidiWrapperMock:
    def __init__(self):
        self.callback = None
        self.connectedDevice = None
        self.sentMessages = list()

    def connect(self, deviceName, callback):
        self.connectedDevice = deviceName
        self.callback = callback

    def disconnect(self):
        self.connectedDevice = None
    
    def sendMessage(self, note, velocity, channel):
        self.sentMessages.append((note, velocity, channel))
    
    def simulateInput(self, note, velocity=127, channel=0):
        if self.callback is not None:
            msg = Message("note_on", note=note, velocity=velocity, channel=channel)
            self.callback(msg)

    def getLatestMessage(self):
        return self.sentMessages[-1]

@pytest.fixture
def playerMock():
    return PlayerMock()

@pytest.fixture
def soloTool(playerMock):
    return SoloTool(playerMock)

@pytest.fixture
def midiWrapperMock():
    return MidiWrapperMock()

@pytest.fixture
def uut(soloTool, midiWrapperMock):
    return MidiController(soloTool, midiWrapperMock)

def test_startStopAndPauseButtons(uut, midiWrapperMock, playerMock):
    uut.connect()

    assert playerMock.state == PlayerMock.STOPPED
    
    midiWrapperMock.simulateInput(playPauseButton)
    assert playerMock.state == PlayerMock.PLAYING
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)

    midiWrapperMock.simulateInput(stopButton)
    assert playerMock.state == PlayerMock.STOPPED
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)

    midiWrapperMock.simulateInput(playPauseButton)
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
    
    midiWrapperMock.simulateInput(playPauseButton)
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)
    assert playerMock.state == PlayerMock.PAUSED

    midiWrapperMock.simulateInput(playPauseButton)
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)
    assert playerMock.state == PlayerMock.PLAYING

    midiWrapperMock.simulateInput(playPauseButton)
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)

    midiWrapperMock.simulateInput(stopButton)
    assert playerMock.state == PlayerMock.STOPPED

def test_startPauseButtonLed(uut, midiWrapperMock, playerMock, soloTool):
    uut.connect()

    assert playerMock.state == PlayerMock.STOPPED
    
    playerMock.state = PlayerMock.PLAYING
    playerMock.simulatePlayingStateChanged()
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)

    playerMock.state = PlayerMock.STOPPED
    playerMock.simulatePlayingStateChanged()
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)

    playerMock.state = PlayerMock.PAUSED
    playerMock.simulatePlayingStateChanged()
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_YELLOW, 0)

    playerMock.state = PlayerMock.PLAYING
    playerMock.simulatePlayingStateChanged()
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, MidiController.LED_GREEN, 0)

def test_jumpToKeyPositionButton(uut, midiWrapperMock, soloTool, playerMock):
    soloTool.addSong("test.flac")
    soloTool.song = 0
    uut.connect()
    
    soloTool.keyPoint = 0.5
    assert playerMock.position == 0.0

    midiWrapperMock.simulateInput(jumpToKeyPositionButton)
    assert playerMock.position == 0.5

def test_previousAndNextSongButtons(uut, midiWrapperMock, soloTool, playerMock):
    songs = [
        "test.flac",
        "test.mp3"
    ]
    for s in songs:
        soloTool.addSong(s)
    uut.connect()

    assert playerMock.currentSong == songs[0]
    midiWrapperMock.simulateInput(nextSongButton)
    assert playerMock.currentSong == songs[1]

    midiWrapperMock.simulateInput(nextSongButton)
    assert playerMock.currentSong == songs[1]

    midiWrapperMock.simulateInput(previousSongButton)
    assert playerMock.currentSong == songs[0]

    midiWrapperMock.simulateInput(previousSongButton)
    assert playerMock.currentSong == songs[0]

def test_previousAndNextKeyPositionButtons(uut, midiWrapperMock, soloTool, playerMock):
    song = "test.flac"
    keyPoints = [0.2, 0.1]

    soloTool.addSong(song)
    soloTool.song = 0
    soloTool.keyPoints = keyPoints

    uut.connect()

    assert soloTool.keyPoint == 0.0

    midiWrapperMock.simulateInput(nextKeyPositionButton)
    assert soloTool.keyPoint == 0.1

    midiWrapperMock.simulateInput(nextKeyPositionButton)
    assert soloTool.keyPoint == 0.2

    midiWrapperMock.simulateInput(previousKeyPositionButton)
    assert soloTool.keyPoint == 0.1

    midiWrapperMock.simulateInput(previousKeyPositionButton)
    assert soloTool.keyPoint == 0.1

def test_playbackRateButtons(uut, midiWrapperMock, soloTool, playerMock):
    playbackRateOptions = {
        16 : (0.5, [LED_YELLOW] * 1 + [LED_OFF] * 7),
        17 : (0.6, [LED_YELLOW] * 2 + [LED_OFF] * 6),
        18 : (0.7, [LED_YELLOW] * 3 + [LED_OFF] * 5),
        19 : (0.8, [LED_YELLOW] * 4 + [LED_OFF] * 4),
        20 : (0.9, [LED_YELLOW] * 5 + [LED_OFF] * 3),
        21 : (1.0, [LED_YELLOW] * 6 + [LED_OFF] * 2),
        22 : (1.1, [LED_YELLOW] * 7 + [LED_OFF] * 1),
        23 : (1.2, [LED_YELLOW] * 8)
    }
    uut.connect()
    assert playerMock.rate == 1.0

    for t, button in enumerate(playbackRateOptions):
        midiWrapperMock.sentMessages.clear()

        midiWrapperMock.simulateInput(button)
        assert playerMock.rate == playbackRateOptions[button][0]

        for i, colour in enumerate(playbackRateOptions[button][1]):
            assert midiWrapperMock.sentMessages[i] == (16 + i, colour, 0)

def test_playbackRateLeds(uut, midiWrapperMock, soloTool, playerMock):
    playbackRateOptions = [
        (0.00, [LED_OFF] * 8),
        (0.49, [LED_OFF] * 8),

        (0.50, [LED_YELLOW] * 1 + [LED_OFF] * 7),
        (0.59, [LED_YELLOW] * 1 + [LED_OFF] * 7),

        (0.60, [LED_YELLOW] * 2 + [LED_OFF] * 6),
        (0.69, [LED_YELLOW] * 2 + [LED_OFF] * 6),
        
        (0.70, [LED_YELLOW] * 3 + [LED_OFF] * 5),
        (0.79, [LED_YELLOW] * 3 + [LED_OFF] * 5),

        (0.80, [LED_YELLOW] * 4 + [LED_OFF] * 4),
        (0.89, [LED_YELLOW] * 4 + [LED_OFF] * 4),

        (0.90, [LED_YELLOW] * 5 + [LED_OFF] * 3),
        (0.99, [LED_YELLOW] * 5 + [LED_OFF] * 3),

        (1.00, [LED_YELLOW] * 6 + [LED_OFF] * 2),
        (1.09, [LED_YELLOW] * 6 + [LED_OFF] * 2),

        (1.10, [LED_YELLOW] * 7 + [LED_OFF] * 1),
        (1.19, [LED_YELLOW] * 7 + [LED_OFF] * 1),

        (1.2, [LED_YELLOW] * 8),
        (1.5, [LED_YELLOW] * 8)
    ]
    uut.connect()
    assert playerMock.rate == 1.0

    for t, (rate, leds) in enumerate(playbackRateOptions):
        print(t)
        midiWrapperMock.sentMessages.clear()

        soloTool.rate = rate
        assert playerMock.rate == rate

        for i, colour in enumerate(leds):
            assert midiWrapperMock.sentMessages[i] == (16 + i, colour, 0)

def test_playbackVolumeButtons(uut, midiWrapperMock, soloTool, playerMock):
    playbackVolumeOptions = {
        0 : (0.5, [LED_GREEN] * 1 + [LED_OFF] * 7),
        1 : (0.6, [LED_GREEN] * 2 + [LED_OFF] * 6),
        2 : (0.7, [LED_GREEN] * 3 + [LED_OFF] * 5),
        3 : (0.8, [LED_GREEN] * 4 + [LED_OFF] * 4),
        4 : (0.9, [LED_GREEN] * 5 + [LED_OFF] * 3),
        5 : (1.0, [LED_GREEN] * 6 + [LED_OFF] * 2),
        6 : (1.1, [LED_GREEN] * 7 + [LED_OFF] * 1),
        7 : (1.2, [LED_GREEN] * 8)
    }
    uut.connect()
    assert playerMock.volume == 1.0
    
    for t, button in enumerate(playbackVolumeOptions):
        midiWrapperMock.sentMessages.clear()

        midiWrapperMock.simulateInput(button)
        assert playerMock.volume == playbackVolumeOptions[button][0]

        for i, colour in enumerate(playbackVolumeOptions[button][1]):
            assert midiWrapperMock.sentMessages[i] == (i, colour, 0)

def test_playbackVolumeLeds(uut, midiWrapperMock, soloTool, playerMock):
    playbackVolumeOptions = [
        (0.00, [LED_OFF] * 8),
        (0.49, [LED_OFF] * 8),

        (0.50, [LED_GREEN] * 1 + [LED_OFF] * 7),
        (0.59, [LED_GREEN] * 1 + [LED_OFF] * 7),

        (0.60, [LED_GREEN] * 2 + [LED_OFF] * 6),
        (0.69, [LED_GREEN] * 2 + [LED_OFF] * 6),
        
        (0.70, [LED_GREEN] * 3 + [LED_OFF] * 5),
        (0.79, [LED_GREEN] * 3 + [LED_OFF] * 5),

        (0.80, [LED_GREEN] * 4 + [LED_OFF] * 4),
        (0.89, [LED_GREEN] * 4 + [LED_OFF] * 4),

        (0.90, [LED_GREEN] * 5 + [LED_OFF] * 3),
        (0.99, [LED_GREEN] * 5 + [LED_OFF] * 3),

        (1.00, [LED_GREEN] * 6 + [LED_OFF] * 2),
        (1.09, [LED_GREEN] * 6 + [LED_OFF] * 2),

        (1.10, [LED_GREEN] * 7 + [LED_OFF] * 1),
        (1.19, [LED_GREEN] * 7 + [LED_OFF] * 1),

        (1.2, [LED_GREEN] * 8),
        (1.5, [LED_GREEN] * 8)
    ]
    uut.connect()
    assert playerMock.volume == 1.0

    for t, (volume, leds) in enumerate(playbackVolumeOptions):
        midiWrapperMock.sentMessages.clear()

        soloTool.volume = volume
        assert playerMock.volume == volume

        for i, colour in enumerate(leds):
            assert midiWrapperMock.sentMessages[i] == (i, colour, 0)

def test_unassignedButton(uut, midiWrapperMock):
    unassignedButton = 48
    uut.connect()

    # expect no crash
    midiWrapperMock.simulateInput(unassignedButton)
    # XXX would be better to assert that nothing changed in the solo tool

def test_connectDisconnect(uut, midiWrapperMock):
    startupMessages = list(
        [(int(i / 8) * 16 + (i % 8), LED_OFF, 0) for i in range(0, 64)] + # clear all
        [(i, LED_GREEN, 0) for i in range(0, 6)] +                      # volume row
        [(i, LED_YELLOW, 0) for i in range(16, 22)] +                   # playback rate row
        [
            (stopButton,                LED_RED, 0),
            (playPauseButton,           LED_YELLOW, 0),
            (jumpToKeyPositionButton,   LED_YELLOW, 0),
            (previousKeyPositionButton, LED_RED, 0),
            (nextKeyPositionButton,     LED_GREEN, 0),
            (setKeyPositionButton,      LED_YELLOW, 0),
            (previousSongButton,        LED_RED, 0),
            (rwd1PcButton,              LED_RED, 0),
            (rwd5PcButton,              LED_RED, 0),
            (rwd25PcButton,             LED_RED, 0),
            (nextSongButton,            LED_GREEN, 0),
            (fwd1PcButton,              LED_GREEN, 0),
            (fwd5PcButton,              LED_GREEN, 0),
            (fwd25PcButton,             LED_GREEN, 0),
        ])

    teardownMessages = [(int(i / 8) * 16 + (i % 8), LED_OFF, 0) for i in range(0, 64)] # clear all

    expectedDevice = "Launchpad Mini MIDI 1"
    uut.connect()

    assert midiWrapperMock.connectedDevice == expectedDevice
    assert set(midiWrapperMock.sentMessages) == set(startupMessages)

    midiWrapperMock.sentMessages.clear()

    uut.disconnect()

    assert set(midiWrapperMock.sentMessages) == set(teardownMessages)

def test_playingFeedbackWhenChangingSong(uut, midiWrapperMock, soloTool, playerMock):
    songs = [
        "test.flac",
        "test.mp3"
    ]
    for s in songs:
        soloTool.addSong(s)
    uut.connect()

    soloTool.song = 0
    soloTool.play()
    assert playerMock.state == PlayerMock.PLAYING
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_GREEN, 0)

    soloTool.song = 1
    assert playerMock.state == PlayerMock.STOPPED
    assert midiWrapperMock.getLatestMessage() == (playPauseButton, LED_YELLOW, 0)

def test_setKeyPositionButton(uut, midiWrapperMock, soloTool, playerMock):
    song = "test.flac"
    soloTool.addSong(song)
    soloTool.song = 0

    uut.connect()

    playerMock.position = 0.3
    midiWrapperMock.simulateInput(setKeyPositionButton)
    assert soloTool.keyPoint == 0.3

    playerMock.position = 0.5
    midiWrapperMock.simulateInput(setKeyPositionButton)
    assert soloTool.keyPoint == 0.5

    playerMock.position = 0.7
    midiWrapperMock.simulateInput(jumpToKeyPositionButton)
    assert playerMock.position == 0.5

def test_seekButtons(uut, midiWrapperMock, soloTool, playerMock):
    song = "test.flac"
    soloTool.addSong(song)
    soloTool.song = 0

    uut.connect()
    
    assert playerMock.position == 0.0

    midiWrapperMock.simulateInput(fwd25PcButton)
    assert playerMock.position == 0.25

    midiWrapperMock.simulateInput(fwd5PcButton)
    assert playerMock.position == 0.30

    midiWrapperMock.simulateInput(fwd1PcButton)
    assert playerMock.position == 0.31

    midiWrapperMock.simulateInput(fwd25PcButton)
    midiWrapperMock.simulateInput(fwd25PcButton)
    midiWrapperMock.simulateInput(fwd25PcButton)
    assert playerMock.position == 1.0

    midiWrapperMock.simulateInput(rwd25PcButton)
    assert playerMock.position == 0.75

    midiWrapperMock.simulateInput(rwd5PcButton)
    assert playerMock.position == 0.70

    midiWrapperMock.simulateInput(rwd1PcButton)
    assert playerMock.position == 0.69

    midiWrapperMock.simulateInput(rwd25PcButton)
    midiWrapperMock.simulateInput(rwd25PcButton)
    midiWrapperMock.simulateInput(rwd25PcButton)
    assert playerMock.position == 0.0