package land.chipmunk.chipmunkmod.song;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;

import net.minecraft.resources.Identifier;
import net.minecraft.util.Util;
import net.minecraft.world.phys.Vec3;

public class NBSConverter {
    public static Instrument[] instrumentIndex = new Instrument[] {
            Instrument.HARP,
            Instrument.BASS,
            Instrument.BASEDRUM,
            Instrument.SNARE,
            Instrument.HAT,
            Instrument.GUITAR,
            Instrument.FLUTE,
            Instrument.BELL,
            Instrument.CHIME,
            Instrument.XYLOPHONE,
            Instrument.IRON_XYLOPHONE,
            Instrument.COW_BELL,
            Instrument.DIDGERIDOO,
            Instrument.BIT,
            Instrument.BANJO,
            Instrument.PLING,
            Instrument.TRUMPET,
            Instrument.TRUMPET_EXPOSED,
            Instrument.TRUMPET_WEATHERED,
            Instrument.TRUMPET_OXIDIZED,
    };

    public static Song getSongFromBytes (final byte[] bytes, final String fileName) throws IOException {
        final ByteBuffer buffer = ByteBuffer.wrap(bytes);
        buffer.order(ByteOrder.LITTLE_ENDIAN);

        short songLength = 0;
        byte format = 0;
        byte vanillaInstrumentCount = 10; // Before v1, we had 10 vanilla instruments
        songLength = buffer.getShort(); // If it's not 0, then it uses the old format
        if (songLength == 0) {
            format = buffer.get();
        }

        if (format >= 1) {
            vanillaInstrumentCount = buffer.get();
        }
        if (format >= 3) {
            songLength = buffer.getShort();
        }

        final short layerCount = buffer.getShort();
        final String songName = getString(buffer, bytes.length);
        final String songAuthor = getString(buffer, bytes.length);
        final String songOriginalAuthor = getString(buffer, bytes.length);
        final String songDescription = getString(buffer, bytes.length);
        final short tempo = buffer.getShort();
        final byte autoSaving = buffer.get();
        final byte autoSavingDuration = buffer.get();
        final byte timeSignature = buffer.get();
        final int minutesSpent = buffer.getInt();
        final int leftClicks = buffer.getInt();
        final int rightClicks = buffer.getInt();
        final int blocksAdded = buffer.getInt();
        final int blocksRemoved = buffer.getInt();
        final String origFileName = getString(buffer, bytes.length);

        byte loop = 0;
        byte maxLoopCount = 0;
        short loopStartTick = 0;
        if (format >= 4) {
            loop = buffer.get();
            maxLoopCount = buffer.get();
            loopStartTick = buffer.getShort();
        }

        final ArrayList<NBSNote> nbsNotes = new ArrayList<>();
        int tick = -1;
        while (true) {
            final int tickJumps = Short.toUnsignedInt(buffer.getShort());
            if (tickJumps == 0) break;
            tick += tickJumps;

            short layer = -1;
            while (true) {
                final int layerJumps = buffer.getShort();
                if (layerJumps == 0) break;
                layer += layerJumps;
                final NBSNote note = new NBSNote();
                note.tick = tick;
                note.layer = layer;
                note.instrument = buffer.get();
                note.key = buffer.get();
                if (format >= 4) {
                    note.velocity = buffer.get();
                    note.panning = buffer.get();
                    note.pitch = buffer.getShort();
                }
                nbsNotes.add(note);
            }
        }

        final ArrayList<NBSLayer> nbsLayers = new ArrayList<>();
        if (buffer.hasRemaining()) {
            for (int i = 0; i < layerCount; i++) {
                final NBSLayer layer = new NBSLayer();
                layer.name = getString(buffer, bytes.length);
                if (format >= 4) {
                    layer.lock = buffer.get();
                }
                layer.volume = buffer.get();
                if (format >= 2) {
                    layer.stereo = buffer.get();
                }
                nbsLayers.add(layer);
            }
        }

        final ArrayList<NBSCustomInstrument> customInstruments = new ArrayList<>();
        if (buffer.hasRemaining()) {
            final byte customInstrumentCount = buffer.get();
            for (int i = 0; i < customInstrumentCount; i++) {
                final String name = getString(buffer, bytes.length);
                final String file = getString(buffer, bytes.length);
                final byte pitch = buffer.get();
                final boolean visible = buffer.get() != 0; // "Press piano key"

                customInstruments.add(new NBSCustomInstrument(name, file, pitch, visible));
            }
        }

        final Song song = new Song(songName.trim().length() > 0 ? songName : fileName);
        if (loop > 0) {
            song.looping = true;
            song.loopPosition = getMilliTime(loopStartTick, tempo);
            song.loopCount = maxLoopCount;
        }
        for (final NBSNote note : nbsNotes) {
            final Instrument instrument;
            double key;
            if (note.instrument < vanillaInstrumentCount) {
                if (note.instrument >= instrumentIndex.length) continue;
                instrument = instrumentIndex[note.instrument];

                key = (double) ((note.key * 100) + note.pitch) / 100;
            } else {
                final int index = note.instrument - vanillaInstrumentCount;
                if (index >= customInstruments.size()) continue;
                final NBSCustomInstrument customInstrument = customInstruments.get(index);

                instrument = customInstrument;
                key = (note.key) + (customInstrument.pitch + (double) note.pitch / 100);
            }

            byte layerVolume = 100;
            if (nbsLayers.size() > note.layer) {
                layerVolume = nbsLayers.get(note.layer).volume;
            }

            while (key < 33) key += 12;
            while (key > 57) key -= 12;

            final double pitch = key - 33;

            final int layerStereo = Byte.toUnsignedInt(nbsLayers.get(note.layer).stereo);
            final int notePanning = Byte.toUnsignedInt(note.panning);

            final double value;

            if (layerStereo == 100 && notePanning != 100) value = notePanning;
            else if (notePanning == 100 && layerStereo != 100) value = layerStereo;
            else value = (double) (layerStereo + notePanning) / 2;

            final double x;

            if (value > 100) x = (value - 100) / -100;
            else if (value == 100) x = 0;
            else x = ((value - 100) * -1) / 100;

            final Vec3 position = new Vec3(x, 0, 0);

            song.add(new Note(instrument, pitch, (float) note.velocity * (float) layerVolume / 10000f, getMilliTime(note.tick, tempo), position));
        }

        song.length = song.get(song.size() - 1).time + 50;

        return song;
    }

    private static String getString (final ByteBuffer buffer, final int maxSize) throws IOException {
        final int length = buffer.getInt();
        if (length > maxSize) {
            throw new IOException("String is too large");
        }
        final byte[] arr = new byte[length];
        buffer.get(arr, 0, length);
        return new String(arr);
    }

    private static int getMilliTime (final int tick, final int tempo) {
        return 1000 * tick * 100 / tempo;
    }

    private static class NBSNote {
        public int tick;
        public short layer;
        public byte instrument;
        public byte key;
        public byte velocity = 100;
        public byte panning = 100;
        public short pitch = 0;
    }

    private static class NBSLayer {
        public String name;
        public byte lock = 0;
        public byte volume;
        public byte stereo = 100;
    }

    private static class NBSCustomInstrument extends Instrument {
        public final byte pitch;
        public final boolean visible;

        public NBSCustomInstrument(final String name, final String optionalPath, final byte pitch, final boolean visible) {
            // some implementations straight up just don't save the path, so we perform conversions on name on those
            final String path = optionalPath.isBlank() ? name : optionalPath;
            String sound = Util.sanitizeName(path, Identifier::isAllowedInIdentifier)
                .replaceFirst("\\.ogg$", "")
                .replaceFirst("^minecraft/", "")
                .replaceFirst("\\d+$", "")
                .replace('/', '.');

            // if there's no path, it's probably a note block sound
            // this happens when you have trumpets saved in a v5 format NBS for example
            if (!sound.contains(".")) sound = "block.note_block." + sound;

            super(-1, name, 0, sound);
            this.pitch = pitch;
            this.visible = visible;
        }
    }
}
