__version__ = "1.0.0-beta.0"


from collections import namedtuple
from struct import Struct

__all__ = [
    "read",
    "new_file",
    "Parser",
    "Writer",
    "File",
    "Header",
    "Note",
    "Layer",
    "Instrument",
]

CURRENT_NBS_VERSION = 5

BYTE = Struct("<B")
SHORT = Struct("<H")
SSHORT = Struct("<h")
INT = Struct("<I")


Instrument = namedtuple("Instrument", ["id", "name", "file", "pitch", "press_key"])


class Note(
    namedtuple(
        "Note", ["tick", "layer", "instrument", "key", "velocity", "panning", "pitch"]
    )
):
    def __new__(cls, tick, layer, instrument, key, velocity=100, panning=0, pitch=0):
        return super().__new__(
            cls, tick, layer, instrument, key, velocity, panning, pitch
        )


class Layer(namedtuple("Layer", ["id", "name", "lock", "volume", "panning"])):
    def __new__(cls, id, name="", lock=False, volume=100, panning=0):
        return super().__new__(cls, id, name, lock, volume, panning)


def read(filename):
    with open(filename, "rb") as buff:
        return Parser(buff).read_file()


def new_file(**header):
    return File(Header(**header), [], [Layer(0, "", False, 100, 0)], [])


class Header(object):
    def __init__(self, **header):
        header_values = {
            "version": header.get("version", CURRENT_NBS_VERSION),
            "default_instruments": header.get("default_instruments", 16),
            "song_length": header.get("song_length", 0),
            "song_layers": header.get("song_layers", 0),
            "song_name": header.get("song_name", ""),
            "song_author": header.get("song_author", ""),
            "original_author": header.get("original_author", ""),
            "description": header.get("description", ""),
            "tempo": header.get("tempo", 10.0),
            "auto_save": header.get("auto_save", False),
            "auto_save_duration": header.get("auto_save_duration", 10),
            "time_signature": header.get("time_signature", 4),
            "minutes_spent": header.get("minutes_spent", 0),
            "left_clicks": header.get("left_clicks", 0),
            "right_clicks": header.get("right_clicks", 0),
            "blocks_added": header.get("blocks_added", 0),
            "blocks_removed": header.get("blocks_removed", 0),
            "song_origin": header.get("song_origin", ""),
            "loop": header.get("loop", False),
            "max_loop_count": header.get("max_loop_count", 0),
            "loop_start": header.get("loop_start", 0),
        }

        for key, value in header_values.items():
            setattr(self, key, value)


class File(object):
    def __init__(self, header, notes, layers, instruments):
        self.header = header
        self.notes = notes
        self.layers = layers
        self.instruments = instruments

    def update_header(self, version):
        self.header.version = version
        if self.notes:
            self.header.song_length = self.notes[-1].tick
        self.header.song_layers = len(self.layers)

    def save(self, filename, version=CURRENT_NBS_VERSION):
        self.update_header(version)
        with open(filename, "wb") as buff:
            Writer(buff).encode_file(self, version)

    def __iter__(self):
        if not self.notes:
            return
        chord = []
        current_tick = self.notes[0].tick

        for note in sorted(self.notes, key=lambda n: n.tick):
            if note.tick == current_tick:
                chord.append(note)
            else:
                chord.sort(key=lambda n: n.layer)
                yield current_tick, chord
                current_tick, chord = note.tick, [note]
        yield current_tick, chord


class Parser(object):
    def __init__(self, buff):
        self.buffer = buff

    def read_file(self):
        values = self.parse_header()
        header = Header(**values)
        version = values["version"]
        return File(
            header,
            list(self.parse_notes(version)),
            list(self.parse_layers(header.song_layers, version)),
            list(self.parse_instruments(version)),
        )

    def read_numeric(self, fmt):
        return fmt.unpack(self.buffer.read(fmt.size))[0]

    def read_string(self):
        length = self.read_numeric(INT)
        return self.buffer.read(length).decode(encoding="cp1252")

    def jump(self):
        value = -1
        while True:
            jump = self.read_numeric(SHORT)
            if not jump:
                break
            value += jump
            yield value

    def parse_header(self):
        song_length = self.read_numeric(SHORT)
        if song_length == 0:
            # A song length of 0 indicates the Open Note Block Studio format
            version = self.read_numeric(BYTE)
        else:
            version = 0

        return {
            "version": version,
            "default_instruments": self.read_numeric(BYTE) if version > 0 else 10,
            "song_length": self.read_numeric(SHORT) if version >= 3 else song_length,
            "song_layers": self.read_numeric(SHORT),
            "song_name": self.read_string(),
            "song_author": self.read_string(),
            "original_author": self.read_string(),
            "description": self.read_string(),
            "tempo": self.read_numeric(SHORT) / 100.0,
            "auto_save": self.read_numeric(BYTE) == 1,
            "auto_save_duration": self.read_numeric(BYTE),
            "time_signature": self.read_numeric(BYTE),
            "minutes_spent": self.read_numeric(INT),
            "left_clicks": self.read_numeric(INT),
            "right_clicks": self.read_numeric(INT),
            "blocks_added": self.read_numeric(INT),
            "blocks_removed": self.read_numeric(INT),
            "song_origin": self.read_string(),
            "loop": self.read_numeric(BYTE) == 1 if version >= 4 else False,
            "max_loop_count": self.read_numeric(BYTE) if version >= 4 else 0,
            "loop_start": self.read_numeric(SHORT) if version >= 4 else 0,
        }

    def parse_notes(self, version):
        for current_tick in self.jump():
            for current_layer in self.jump():
                instrument = self.read_numeric(BYTE)
                key = self.read_numeric(BYTE)
                velocity = self.read_numeric(BYTE) if version >= 4 else 100
                panning = self.read_numeric(BYTE) - 100 if version >= 4 else 0
                pitch = self.read_numeric(SSHORT) if version >= 4 else 0
                yield Note(
                    current_tick,
                    current_layer,
                    instrument,
                    key,
                    velocity,
                    panning,
                    pitch,
                )

    def parse_layers(self, layers_count, version):
        for i in range(layers_count):
            name = self.read_string()
            lock = self.read_numeric(BYTE) == 1 if version >= 4 else False
            volume = self.read_numeric(BYTE)
            panning = self.read_numeric(BYTE) - 100 if version >= 2 else 0
            yield Layer(i, name, lock, volume, panning)

    def parse_instruments(self, version):
        for i in range(self.read_numeric(BYTE)):
            name = self.read_string()
            sound_file = self.read_string()
            pitch = self.read_numeric(BYTE)
            press_key = self.read_numeric(BYTE) == 1
            yield Instrument(i, name, sound_file, pitch, press_key)


class Writer(object):
    def __init__(self, buff):
        self.buffer = buff

    def encode_file(self, nbs_file, version):
        self.write_header(nbs_file, version)
        self.write_notes(nbs_file, version)
        self.write_layers(nbs_file, version)
        self.write_instruments(nbs_file, version)

    def encode_numeric(self, fmt, value):
        self.buffer.write(fmt.pack(value))

    def encode_string(self, value):
        self.encode_numeric(INT, len(value))
        self.buffer.write(value.encode(encoding="cp1252"))

    def write_header(self, nbs_file, version):
        header = nbs_file.header

        if version > 0:
            self.encode_numeric(SHORT, 0)
            self.encode_numeric(BYTE, version)
            self.encode_numeric(BYTE, header.default_instruments)
        else:
            self.encode_numeric(SHORT, header.song_length)
        if version >= 3:
            self.encode_numeric(SHORT, header.song_length)
        self.encode_numeric(SHORT, header.song_layers)
        self.encode_string(header.song_name)
        self.encode_string(header.song_author)
        self.encode_string(header.original_author)
        self.encode_string(header.description)

        self.encode_numeric(SHORT, int(header.tempo * 100))
        self.encode_numeric(BYTE, int(header.auto_save))
        self.encode_numeric(BYTE, header.auto_save_duration)
        self.encode_numeric(BYTE, header.time_signature)

        self.encode_numeric(INT, header.minutes_spent)
        self.encode_numeric(INT, header.left_clicks)
        self.encode_numeric(INT, header.right_clicks)
        self.encode_numeric(INT, header.blocks_added)
        self.encode_numeric(INT, header.blocks_removed)
        self.encode_string(header.song_origin)

        if version >= 4:
            self.encode_numeric(BYTE, int(header.loop))
            self.encode_numeric(BYTE, header.max_loop_count)
            self.encode_numeric(SHORT, header.loop_start)

    def write_notes(self, nbs_file, version):
        current_tick = -1

        for tick, chord in nbs_file:
            self.encode_numeric(SHORT, tick - current_tick)
            current_tick = tick
            current_layer = -1

            for note in chord:
                self.encode_numeric(SHORT, note.layer - current_layer)
                current_layer = note.layer
                self.encode_numeric(BYTE, note.instrument)
                self.encode_numeric(BYTE, note.key)
                if version >= 4:
                    self.encode_numeric(BYTE, note.velocity)
                    self.encode_numeric(BYTE, note.panning + 100)
                    self.encode_numeric(SSHORT, note.pitch)

            self.encode_numeric(SHORT, 0)
        self.encode_numeric(SHORT, 0)

    def write_layers(self, nbs_file, version):
        for layer in nbs_file.layers:
            self.encode_string(layer.name)
            if version >= 4:
                self.encode_numeric(BYTE, int(layer.lock))
            self.encode_numeric(BYTE, layer.volume)
            if version >= 2:
                self.encode_numeric(BYTE, layer.panning + 100)

    def write_instruments(self, nbs_file, version):
        self.encode_numeric(BYTE, len(nbs_file.instruments))
        for instrument in nbs_file.instruments:
            self.encode_string(instrument.name)
            self.encode_string(instrument.file)
            self.encode_numeric(BYTE, instrument.pitch)
            self.encode_numeric(BYTE, int(instrument.press_key))
