using System.Collections.Generic;
using UnityEngine;

// 표준 MIDI 파일(.mid)을 파싱해 Note 리스트로 변환하는 미니 파서.
// 단일 템포 가정. Note-On(velocity>0) 이벤트만 사용한다.
public static class MidiChartParser
{
    // data: .mid 원본 바이트 / offset: 곡 시작 보정(초)
    // bpmOverride: 0이면 MIDI 내장 템포 사용, >0이면 이 BPM으로 강제
    // lanePitches: 비우면 모든 노트를 레인 0으로, 채우면 해당 음높이만 레인으로 매핑
    public static List<Note> Parse(byte[] data, float offset, float bpmOverride, List<int> lanePitches)
    {
        var notes = new List<Note>();
        if (data == null || data.Length < 14)
        {
            Debug.LogWarning("[MidiChartParser] MIDI 데이터가 비었거나 너무 짧습니다.");
            return notes;
        }

        int p = 0;

        // ---- 헤더 청크 (MThd) ----
        if (!MatchId(data, p, 'M', 'T', 'h', 'd'))
        {
            Debug.LogWarning("[MidiChartParser] MThd 헤더가 아닙니다. (TextAsset이 .mid가 맞는지 확인)");
            return notes;
        }
        p += 4;
        ReadUInt32(data, ref p);              // 헤더 길이(=6), 사용 안 함
        ReadUInt16(data, ref p);              // 포맷
        int trackCount = ReadUInt16(data, ref p);
        int division = ReadUInt16(data, ref p);

        if ((division & 0x8000) != 0)
            Debug.LogWarning("[MidiChartParser] SMPTE 타임코드는 미지원입니다. PPQN으로 가정합니다.");
        int ticksPerQuarter = division & 0x7FFF;
        if (ticksPerQuarter <= 0) ticksPerQuarter = 480;

        // (절대틱, 음높이) 수집 + 첫 템포
        var rawNotes = new List<(int tick, int pitch)>();
        int tempoMicros = -1;   // 4분음표당 마이크로초 (미발견 시 -1)

        // ---- 트랙 청크들 (MTrk) ----
        for (int t = 0; t < trackCount && p + 8 <= data.Length; t++)
        {
            if (!MatchId(data, p, 'M', 'T', 'r', 'k'))
                break;
            p += 4;
            long len = ReadUInt32(data, ref p);
            int trackEnd = p + (int)len;
            if (trackEnd > data.Length) trackEnd = data.Length;

            int absTicks = 0;
            int runningStatus = 0;

            while (p < trackEnd)
            {
                absTicks += ReadVarLen(data, ref p);

                int status = data[p];
                if (status < 0x80)
                {
                    status = runningStatus;   // 러닝 스테이터스: 상태 바이트 생략
                    if (status == 0) { p = trackEnd; break; }  // 손상된 데이터 방어
                }
                else
                {
                    p++;
                    runningStatus = status;
                }

                if (status == 0xFF)           // 메타 이벤트
                {
                    int metaType = data[p++];
                    int mlen = ReadVarLen(data, ref p);
                    if (metaType == 0x51 && mlen == 3 && tempoMicros < 0)
                        tempoMicros = (data[p] << 16) | (data[p + 1] << 8) | data[p + 2];
                    p += mlen;
                    runningStatus = 0;
                }
                else if (status == 0xF0 || status == 0xF7)   // SysEx
                {
                    int slen = ReadVarLen(data, ref p);
                    p += slen;
                    runningStatus = 0;
                }
                else                          // 채널 메시지
                {
                    int type = status & 0xF0;
                    switch (type)
                    {
                        case 0x90:            // Note-On
                            int pitch = data[p++];
                            int velocity = data[p++];
                            if (velocity > 0) rawNotes.Add((absTicks, pitch));
                            break;
                        case 0x80:            // Note-Off
                        case 0xA0:            // 폴리 애프터터치
                        case 0xB0:            // 컨트롤 체인지
                        case 0xE0:            // 피치 벤드
                            p += 2;
                            break;
                        case 0xC0:            // 프로그램 체인지
                        case 0xD0:            // 채널 애프터터치
                            p += 1;
                            break;
                        default:
                            p = trackEnd;     // 알 수 없는 상태 → 트랙 종료(무한루프 방지)
                            break;
                    }
                }
            }

            p = trackEnd;   // 트랙 경계 보정
        }

        // [임시 디버그] MIDI에 실제로 들어있는 고유 음높이 확인 (LanePitches 번호 맞춘 뒤 삭제)
        var uniquePitches = new HashSet<int>();
        foreach (var raw in rawNotes) uniquePitches.Add(raw.pitch);
        var sortedPitches = new List<int>(uniquePitches);
        sortedPitches.Sort();
        Debug.Log($"[MidiChartParser] 발견된 고유 pitch: {string.Join(", ", sortedPitches)} (노트 {rawNotes.Count}개)");

        // ---- 틱 → 초 변환 ----
        double usPerQuarter = bpmOverride > 0f
            ? 60000000.0 / bpmOverride
            : (tempoMicros > 0 ? tempoMicros : 500000.0);   // 기본 120 BPM
        double secPerTick = usPerQuarter / 1000000.0 / ticksPerQuarter;

        bool useLanes = lanePitches != null && lanePitches.Count > 0;

        foreach (var raw in rawNotes)
        {
            int lane = 0;
            if (useLanes)
            {
                lane = lanePitches.IndexOf(raw.pitch);
                if (lane < 0) continue;   // 레인에 매핑 안 된 음높이는 무시
            }

            notes.Add(new Note
            {
                Time = offset + (float)(raw.tick * secPerTick),
                Lane = lane
            });
        }

        notes.Sort((a, b) => a.Time.CompareTo(b.Time));
        return notes;
    }

    // ---- 바이트 읽기 헬퍼 (MIDI는 빅엔디안) ----

    private static bool MatchId(byte[] d, int p, char a, char b, char c, char e)
        => p + 4 <= d.Length && d[p] == a && d[p + 1] == b && d[p + 2] == c && d[p + 3] == e;

    private static uint ReadUInt32(byte[] d, ref int p)
    {
        uint v = (uint)((d[p] << 24) | (d[p + 1] << 16) | (d[p + 2] << 8) | d[p + 3]);
        p += 4;
        return v;
    }

    private static int ReadUInt16(byte[] d, ref int p)
    {
        int v = (d[p] << 8) | d[p + 1];
        p += 2;
        return v;
    }

    // 가변 길이 수치(Variable-Length Quantity)
    private static int ReadVarLen(byte[] d, ref int p)
    {
        int value = 0;
        byte b;
        do
        {
            b = d[p++];
            value = (value << 7) | (b & 0x7F);
        }
        while ((b & 0x80) != 0 && p < d.Length);
        return value;
    }
}
