import os
import json
from pathlib import Path
from importlib import resources
from functools import cached_property
from dataclasses import dataclass, fields
from atonatui.constants import RESET, REVERSE, SOUNDFONTS

_INSTRUMENT_LIST = [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(SOUNDFONTS.keys())]
_USER_SAVE_DIR = Path.home() / ".local" / "share" / "atonatui" / "saves"

try:
    _DEFAULT_SETTINGS_PATH = resources.files("atonatui").joinpath("assets/settings.json")
except Exception:
    _DEFAULT_SETTINGS_PATH = os.path.join(os.path.dirname(__file__), "assets", "settings.json")

_USER_CONFIG_DIR = Path.home() / ".config" / "atonatui"
_SETTINGS_SAVE_FILE = _USER_CONFIG_DIR / "settings.json"

_MATRIX_SAVE_DIR = Path.home() / ".local" / "share" / "atonatui" / "saves"


def get_saved_matrix_list():
    if not _MATRIX_SAVE_DIR.exists():
        return ["  [No saved matrices found]"]

    save_dir_str = str(_MATRIX_SAVE_DIR)

    save_files = [
        f for f in os.listdir(save_dir_str)
        if os.path.isfile(os.path.join(save_dir_str, f)) and f.endswith(".sav")
    ]
    save_files.sort()

    if not save_files:
        return ["  [No saved matrices found]"]

    return [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(save_files)]


_SAVED_MATRIX_LIST = get_saved_matrix_list()


@dataclass
class Settings:
    BPM: int = 140
    INSTRUMENT: str = "Xylophone"
    OCTAVE: int = 3
    OCTAVE_RANDOMIZATION: int = 0
    RHYTHM_COMPLEXITY: int = 0
    DEFAULT_DIVISION: int = 2
    REST_PROBABILITY: int = 0
    SAVE_SETTINGS: None = None
    SAVE_MATRIX: None = None
    LOAD_MATRIX: None = None

    @classmethod
    def max_len(cls):
        return max(len(f.name) for f in fields(cls)) + 2


@dataclass
class SettingsHelp:
    BPM: str = "Enter a BPM between 30 - 300"
    INSTRUMENT: str = f"Select Soundfont:\n\n{'\n'.join(_INSTRUMENT_LIST)}"
    OCTAVE: str = "Change the octave. Select an octave between 1 and 5"
    OCTAVE_RANDOMIZATION: str = "\n[0] Only play base octave\n[1] Randomize octaves, increasing octave range by +1 and -1 (example: base octave = 3, octave range = 2-4)\n[2] Randomize octaves, increasing octave range by +2 and -2 (example: base octave = 3, octave range = 1-5)\n[3] Randomize octaves, increasing octave range by +3 and -3 (example: base octave = 1, octave range = 1-4)\n\nWhen the range would result in an octave higher than 5 or lower than 0, it clamps range to fit 0-5"
    RHYTHM_COMPLEXITY: str = "\n[0] Off, Use DEFAULT_DIVISION\n[1] On, Engage Rhythm Matrix\n[2] On, Rhythm Matrix and Rest Randomization\n[3] On, Chaos Mode, A random duration is chosen between 0.05 seconds and 1 second"
    DEFAULT_DIVISION: str = "\n[0] Whole Note\n[1] Half Note\n[2] Quarter Note\n[3] Eight Note"
    REST_PROBABILITY: str = "Probability in which a note in row will be replaced by a rest.\nEnter a value between 0 and 100, where 0 is absolutely no chance of a rest and 100 is all rests.\nThis only works in RHYTHM_COMPLEXITY MODE 2."
    SAVE_SETTINGS: str = "Save current settings to file"
    SAVE_MATRIX: str = "Save current matrix to file, Enter name of matrix to save"
    LOAD_MATRIX: str = f"Select Saved Matrix to Load:\n\n{'\n'.join(_SAVED_MATRIX_LIST)}"
    EXIT: None = None


SETTINGS_RULES = {
    "BPM": {"type": int, "min": 30, "max": 300},
    "OCTAVE": {"type": int, "min": 1, "max": 5},
    "OCTAVE_RANDOMIZATION": {"type": int, "min": 0, "max": 4},
    "RHYTHM_COMPLEXITY": {"type": int, "min": 0, "max": 3},
    "DEFAULT_DIVISION": {"type": int, "min": 0, "max": 3},
    "REST_PROBABILITY": {"type": int, "min": 0, "max": 100},
    "INSTRUMENT": {"type": int, "min": 0, "max": len(SOUNDFONTS)},
    "SAVE_MATRIX": {"type": str, "min": 0, "max": 10},
    "LOAD_MATRIX": {"type": str, "min": 0, "max": 100},
}


class SettingsMixin:

    @cached_property
    def settings(self):
        return [f.name for f in fields(Settings)]

    def save_settings(self):
        _USER_CONFIG_DIR.mkdir(parents=True, exist_ok=True)

        setting_obj = {}
        for field in fields(Settings):
            setting_obj[field.name] = getattr(Settings, field.name)

        with open(_SETTINGS_SAVE_FILE, "w+") as f:
            json.dump(setting_obj, f)

    def load_settings(self):
        target_file = _SETTINGS_SAVE_FILE if _SETTINGS_SAVE_FILE.exists() else _DEFAULT_SETTINGS_PATH

        with open(target_file, "r") as f:
            data = json.load(f)
            for k, v in data.items():
                setattr(Settings, k, int(v) if str(v).isdigit() else v)

            self.update_sf(SOUNDFONTS[Settings.INSTRUMENT])
            self.bpm = int(Settings.BPM)

    def display_settings(self):
        self.clear_screen()
        print(f"{REVERSE}  SETTINGS:                          {RESET}\n")
        for idx, setting in enumerate(self.settings):
            value = getattr(Settings, setting)
            print(f"[{idx}] {setting.ljust(Settings.max_len())}{": " + str(value) if value != None else ''}")

        self.user_input()

        self.display_settings()

    def user_input(self, setting_name=None):
        request = self.request('Enter ID of setting you want to change, "q" to exit')

        if request[0] == "q":
            self.loop()

        request = self.validate_request(request)

        setting_name = self.settings[request]

        if setting_name == "SAVE_SETTINGS":
            self.save_settings()
            self.message("Settings Saved..", self.display_settings, type="SUCCESS")

        self.display_setting(setting_name)

        value = self.request("Update setting by entering value, 'q' to exit")

        value = self.validate_new_setting(setting_name, value)

        setattr(Settings, setting_name, value)

        if setting_name == "LOAD_MATRIX":
            self.handle_load_matrix()

        if setting_name == "SAVE_MATRIX":
            self.handle_save_matrix()

        if setting_name == "BPM":
            self.bpm = int(Settings.BPM)

        if setting_name == "INSTRUMENT":
            instrument = list(SOUNDFONTS.keys())[value]
            setattr(Settings, setting_name, instrument)
            self.update_sf(SOUNDFONTS[instrument])

        if setting_name == "OCTAVE_RANDOMIZATION":
            del self.octave_row
            del self.octave_map
            _ = self.octave_row
            _ = self.octave_map

        if setting_name == "OCTAVE":
            del self.octave_row
            del self.octave_map
            _ = self.octave_row
            _ = self.octave_map

    def validate_request(self, request):
        try:
            request = int(request)
            if request >= len(self.settings):
                raise ValueError
            return request
        except ValueError:
            self.message(f"'{request}' is not a valid input, try again!", self.display_settings)

    def display_setting(self, setting_name):
        self.clear_screen()
        print(f"{REVERSE}{setting_name.ljust(Settings.max_len())} CURRENT VALUE: {getattr(Settings, setting_name)}   {RESET}\n")
        print(f"INFO: {getattr(SettingsHelp, setting_name)}\n")

    def validate_new_setting(self, setting_name, value):
        if value == "q":
            self.display_settings()

        try:
            if SETTINGS_RULES[setting_name]['type'] == int:
                value = int(value)
                if value >= SETTINGS_RULES[setting_name]['min'] and value <= SETTINGS_RULES[setting_name]['max']:
                    pass
                else:
                    raise ValueError
                return value
            else:
                return value
        except ValueError:
            self.message(f"'{value}' is not a valid input, try again!", self.display_settings)

    def handle_load_matrix(self):
        file_name = " ".join(_SAVED_MATRIX_LIST[int(Settings.LOAD_MATRIX)].split("] ")[1:])

        target_file = _USER_SAVE_DIR / file_name

        with open(target_file, 'r') as f:
            seed = f.readline()

        self.current_seed = int(seed)
        self.regenerate()
        self.use_current_seed = True

    def handle_save_matrix(self):
        _USER_SAVE_DIR.mkdir(parents=True, exist_ok=True)

        cleaned_name = Settings.SAVE_MATRIX.strip().replace(".", " ").replace(" ", "_")
        Settings.SAVE_MATRIX = cleaned_name

        target_file = _USER_SAVE_DIR / f"{cleaned_name}.sav"

        with open(target_file, 'w+') as f:
            f.write(str(self.current_seed))
