import random
from collections import defaultdict
from functools import cached_property

from atonatui.constants import NOTES, RESET, REVERSE, MAGENTA, GREEN, YELLOW, BOLD, WHITE
from atonatui.audio_mixin import AudioMixin
from atonatui.display_mixin import DisplayMixin
from atonatui.settings_mixin import Settings, SettingsMixin
from atonatui.input_mixin import InputMixin


class ToneRow(AudioMixin, DisplayMixin, SettingsMixin, InputMixin):
    def __init__(self):
        AudioMixin.__init__(self)
        self.bpm = Settings.BPM

        self.pr_map = {}
        self.pr_map_populated = False

        self.inv_map = {}
        self.inv_map_populated = False

        self.current_seed = None
        self.use_current_seed = False
        self.current_rhythm_seed = None
        self.use_current_rhythm_seed = False
        self.current_octave_seed = None
        self.use_current_octave_seed = False

        self.rhythm_to_divison_map = {
            0: self.whole,
            1: self.half,
            2: self.quarter,
            3: self.eigth
        }

        self.generate()
        self.load_settings()

    @cached_property
    def octaves(self):
        return [str(Settings.OCTAVE)] * 12

    @property
    def eigth(self):
        return self.quarter / 2

    @property
    def quarter(self):
        return 60.0 / self.bpm

    @property
    def half(self):
        return self.quarter * 2

    @property
    def whole(self):
        return self.half * 2

    @cached_property
    def tone_row(self):
        if not self.use_current_seed:
            self.current_seed = random.randint(100000, 999999)
        else:
            self.use_current_seed = False

        random.seed(self.current_seed)

        notes = NOTES.copy()
        random.shuffle(notes)

        random.seed(None)
        return notes

    @cached_property
    def reordered_notes(self):
        first_note_idx = NOTES.index(self.tone_row[0])
        notes = NOTES.copy()
        return notes[first_note_idx:] + notes[0:first_note_idx]

    @cached_property
    def prime(self):
        prime = []
        for tone in self.tone_row:
            prime.append(self.reordered_notes.index(tone))
        return prime

    @cached_property
    def inversion(self):
        inversion = []
        for note in self.prime:
            inversion.append((2 * self.prime[0] - note) % 12)
        return inversion

    @cached_property
    def matrix(self):
        matrix = []

        for shift in self.inversion:
            row = []
            for item in self.prime:
                x = (item + shift) % 12
                row.append(x)
            matrix.append(row)

        return matrix

    @cached_property
    def string_matrix(self):
        smatrix = []
        for row in self.matrix:
            new_row = []
            for tone in row:
                new_row.append(self.reordered_notes[tone])
            smatrix.append(new_row)
        return smatrix

    @cached_property
    def rhythm_row(self):
        if not self.use_current_rhythm_seed:
            self.current_rhythm_seed = random.randint(100000, 999999)
        else:
            self.use_current_rhythm_seed = False

        random.seed(self.current_rhythm_seed)
        roptions = [0, 1, 2, 3]
        random.shuffle(roptions)
        random.seed(None)
        return roptions * 3

    @cached_property
    def rhythm_map(self):
        prime = self.string_matrix[0]
        rmap = defaultdict(int)

        for r, t in zip(self.rhythm_row, prime):
            rmap[t] = r

        return rmap

    @cached_property
    def octave_row(self):
        if not self.use_current_octave_seed:
            self.current_octave_seed = random.randint(100000, 999999)
        else:
            self.use_current_octave_seed = False

        random.seed(self.current_octave_seed)

        octave_options = [Settings.OCTAVE]

        for i in range(Settings.OCTAVE_RANDOMIZATION):
            positive = Settings.OCTAVE + (i + 1)
            if positive > 5:
                positive = 5
            if positive not in octave_options:
                octave_options.append(positive)

            negative = Settings.OCTAVE - (i + 1)
            if negative < 1:
                negative = 1
            if negative not in octave_options:
                octave_options.append(negative)

        random.shuffle(octave_options)

        octave_row = []

        left = 0
        while len(octave_row) < 12:
            octave_row.append(octave_options[left])
            left += 1
            if left > len(octave_options) - 1:
                left = 0

        random.seed(None)
        return octave_row

    @cached_property
    def octave_map(self):
        prime = self.string_matrix[0]
        omap = defaultdict(int)

        for o, t in zip(self.octave_row, prime):
            omap[t] = o

        return omap

    def regenerate(self):
        del self.tone_row
        del self.reordered_notes
        del self.prime
        del self.inversion
        del self.matrix
        del self.string_matrix
        del self.octaves
        del self.rhythm_row
        del self.rhythm_map
        del self.octave_row
        del self.octave_map
        self.pr_map_populated = False
        self.inv_map_populated = False
        self.generate()

    def generate(self):
        _ = self.tone_row
        _ = self.reordered_notes
        _ = self.prime
        _ = self.inversion
        _ = self.matrix
        _ = self.string_matrix
        _ = self.octaves
        _ = self.rhythm_row
        _ = self.rhythm_map
        _ = self.octave_row
        _ = self.octave_map

    def play_row(self, row):
        division_map = {
            3: self.eigth,
            2: self.quarter,
            1: self.half,
            0: self.whole
        }

        division = division_map[Settings.DEFAULT_DIVISION]

        for idx, note in enumerate(row):
            padded_tone_row = [f'{BOLD}{REVERSE}{MAGENTA}TONE']
            padded_rhythm_row = [f'{BOLD}{REVERSE}{YELLOW}RTHM']
            padded_octave_row = [f'{BOLD}{REVERSE}{GREEN}OCTV']

            for idx2, x in enumerate(row):
                if idx == idx2:
                    padded_tone_row.append(f"{REVERSE}{WHITE}{x.ljust(3)}{RESET}{REVERSE}{MAGENTA}{BOLD}")
                    padded_rhythm_row.append(f"{REVERSE}{WHITE}{str(self.rhythm_map[x]).ljust(3)}{RESET}{REVERSE}{YELLOW}{BOLD}")
                    padded_octave_row.append(f"{REVERSE}{WHITE}{str(self.octave_map[x]).ljust(3)}{RESET}{REVERSE}{GREEN}{BOLD}")
                else:
                    padded_tone_row.append(x.ljust(3))
                    padded_rhythm_row.append(str(self.rhythm_map[x]).ljust(3))
                    padded_octave_row.append(str(self.octave_map[x]).ljust(3))

            self.clear_screen()
            self.display(play_mode=True)
            print(" | ".join(padded_tone_row) + f"     {RESET}")
            print(" | ".join(padded_rhythm_row) + f"     {RESET}")
            print(" | ".join(padded_octave_row) + f"     {RESET}")

            if Settings.RHYTHM_COMPLEXITY == 0:
                self.play(note + str(self.octave_map[note]), division)

            if Settings.RHYTHM_COMPLEXITY in [1, 2]:
                rest = False
                if Settings.RHYTHM_COMPLEXITY == 2:
                    dice_roll = random.randint(1, 100)
                    if dice_roll <= Settings.REST_PROBABILITY:
                        rest = True

                division = self.rhythm_to_divison_map[self.rhythm_map[note]]
                self.play(note + str(self.octave_map[note]), division, rest=rest)

            if Settings.RHYTHM_COMPLEXITY == 3:
                division = random.uniform(0.05, 1)
                self.play(note + str(self.octave_map[note]), division)


def main():
    t = ToneRow()
    t.loop()


if __name__ == "__main__":
    main()
