#!/usr/bin/env python3
import sys

commands = {}
commands['seq'] = {
    # non-arg commands
    0xff: ['end'],
    0xfe: ['delay1'],
    0xfd: ['delay', 'var'],
    0xfc: ['call', 'addr'],
    0xfb: ['jump', 'addr'],
    0xfa: ['beqz', 'addr'],
    0xf9: ['bltz', 'addr'],
    0xf8: ['loop', 'u8'],
    0xf7: ['loopend'],
    0xf5: ['bgez', 'addr'],
    0xf2: ['reservenotes', 'u8'],
    0xf1: ['unreservenotes'],
    0xdf: ['transpose', 's8'],
    0xde: ['transposerel', 's8'],
    0xdd: ['settempo', 'u8'],
    0xdc: ['addtempo', 's8'],
    0xdb: ['setvol', 'u8'],
    0xda: ['changevol', 's8'],
    0xd7: ['initchannels', 'hex16'],
    0xd6: ['disablechannels', 'hex16'],
    0xd5: ['setmutescale', 's8'],
    0xd4: ['mute'],
    0xd3: ['setmutebhv', 'hex8'],
    0xd2: ['setshortnotevelocitytable', 'addr'],
    0xd1: ['setshortnotedurationtable', 'addr'],
    0xd0: ['setnoteallocationpolicy', 'u8'],
    0xcc: ['setval', 'u8'],
    0xc9: ['bitand', 'u8'],
    0xc8: ['subtract', 'u8'],
    # arg commands
    0x00: ['testchdisabled', 'arg'],
    0x50: ['subvariation', 'ign-arg'],
    0x70: ['setvariation', 'ign-arg'],
    0x80: ['getvariation', 'ign-arg'],
    0x90: ['startchannel', 'arg', 'addr'],
}

commands['chan'] = {
    # non-arg commands
    0xff: ['end'],
    0xfe: ['delay1'],
    0xfd: ['delay', 'var'],
    0xfc: ['call', 'addr'],
    0xfb: ['jump', 'addr'],
    0xfa: ['beqz', 'addr'],
    0xf9: ['bltz', 'addr'],
    0xf8: ['loop', 'u8'],
    0xf7: ['loopend'],
    0xf6: ['break'],
    0xf5: ['bgez', 'addr'],
    0xf3: ['hang'],
    0xf2: ['reservenotes', 'u8'],
    0xf1: ['unreservenotes'],
    0xe4: ['dyncall'],
    0xe3: ['setvibratodelay', 'u8'],
    0xe2: ['setvibratoextentlinear', 'u8', 'u8', 'u8'],
    0xe1: ['setvibratoratelinear', 'u8', 'u8', 'u8'],
    0xe0: ['setvolscale', 'u8'],
    0xdf: ['setvol', 'u8'],
    0xde: ['freqscale', 'u16'],
    0xdd: ['setpan', 'u8'],
    0xdc: ['setpanmix', 'u8'],
    0xdb: ['transpose', 's8'],
    0xda: ['setenvelope', 'addr'],
    0xd9: ['setdecayrelease', 'u8'],
    0xd8: ['setvibratoextent', 'u8'],
    0xd7: ['setvibratorate', 'u8'],
    0xd6: ['setupdatesperframe_unimplemented', 'u8'],
    0xd4: ['setreverb', 'u8'],
    0xd3: ['pitchbend', 's8'],
    0xd2: ['setsustain', 'u8'],
    0xd1: ['setnoteallocationpolicy', 'u8'],
    0xd0: ['stereoheadseteffects', 'u8'],
    0xcc: ['setval', 'u8'],
    0xcb: ['readseq', 'addr'],
    0xca: ['setmutebhv', 'hex8'],
    0xc9: ['bitand', 'u8'],
    0xc8: ['subtract', 'u8'],
    0xc7: ['writeseq', 'u8', 'addr'],
    0xc6: ['setbank', 'u8'],
    0xc5: ['dynsetdyntable'],
    0xc4: ['largenoteson'],
    0xc3: ['largenotesoff'],
    0xc2: ['setdyntable', 'addr'],
    0xc1: ['setinstr', 'u8'],
    # arg commands
    0x00: ['testlayerfinished', 'arg'],
    0x10: ['startchannel', 'arg', 'addr'],
    0x20: ['disablechannel', 'arg'],
    0x30: ['iowriteval2', 'arg', 'u8'],
    0x40: ['ioreadval2', 'arg', 'u8'],
    0x50: ['ioreadvalsub', 'arg'],
    0x60: ['setnotepriority', 'arg'],
    0x70: ['iowriteval', 'arg'],
    0x80: ['ioreadval', 'arg'],
    0x90: ['setlayer', 'arg', 'addr'],
    0xa0: ['freelayer', 'arg'],
    0xb0: ['dynsetlayer', 'arg'],
}

commands_layer_base = {
    # non-arg commands
    0xc0: ['delay', 'var'],
    0xc1: ['setshortnotevelocity', 'u8'],
    0xc2: ['transpose', 'u8'],
    0xc3: ['setshortnotedefaultplaypercentage', 'var'],
    0xc4: ['somethingon'], # ?? (something to do with decay behavior)
    0xc5: ['somethingoff'], # ??
    0xc6: ['setinstr', 'u8'],
    0xc7: ['portamento', 'hex8', 'u8', 'u8'],
    0xc8: ['disableportamento'],
    0xc9: ['setshortnoteduration', 'u8'],
    0xca: ['setpan', 'u8'],
    0xf7: ['loopend'],
    0xf8: ['loop', 'u8'],
    0xfb: ['jump', 'addr'],
    0xfc: ['call', 'addr'],
    0xff: ['end'],
    # arg commands
    0xd0: ['setshortnotevelocityfromtable', 'arg'],
    0xe0: ['setshortnotedurationfromtable', 'arg'],
}

commands['layer_large'] = dict(list(commands_layer_base.items()) + list({
    0x00: ['note0', 'arg', 'var', 'u8', 'u8'],
    0x40: ['note1', 'arg', 'var', 'u8'],
    0x80: ['note2', 'arg', 'u8', 'u8'],
}.items()))

commands['layer_small'] = dict(list(commands_layer_base.items()) + list({
    0x00: ['smallnote0', 'arg', 'var'],
    0x40: ['smallnote1', 'arg'],
    0x80: ['smallnote2', 'arg'],
}.items()))

print_end_padding = False
if "--print-end-padding" in sys.argv:
    print_end_padding = True
    sys.argv.remove("--print-end-padding")

if len(sys.argv) != 2:
    print(f"Usage: {sys.argv[0]} (--emit-asm-macros | input.m64)")
    sys.exit(0)

if sys.argv[1] == "--emit-asm-macros":
    print("# Macros for disassembled sequence files. This file was automatically generated by seq_decoder.py.")
    print("# To regenerate it, run: ./tools/seq_decoder.py --emit-asm-macros >seq_macros.inc")
    print()
    def print_hword(x):
        print(f"    .byte {x} >> 8, {x} & 0xff")

    def emit_cmd(key, op, cmd):
        mn = cmd[0]
        args = cmd[1:]
        param_names = []
        param_list = []
        nibble_param_name = None
        for i, arg in enumerate(args):
            param_name = chr(97 + i)
            param_names.append(param_name)
            param_list.append(param_name + ("=0" if arg == "ign-arg" else ""))
            if arg == "ign-arg" or arg == "arg":
                nibble_param_name = param_name
        print(f".macro {key}_{mn} {', '.join(param_list)}".rstrip())
        if nibble_param_name is not None:
            print(f"    .byte {hex(op)} + \\{nibble_param_name}")
        else:
            print(f"    .byte {hex(op)}")
        for arg, param_name in zip(args, param_names):
            if arg in ['arg', 'ign-arg']:
                pass
            elif arg in ['s8', 'u8', 'hex8']:
                print(f"    .byte \\{param_name}")
            elif arg in ['u16', 'hex16']:
                print_hword("\\" + param_name)
            elif arg == 'addr':
                print_hword(f"(\\{param_name} - sequence_start)")
            elif arg == 'var_long':
                print(f"    var_long \\{param_name}")
            elif arg == 'var':
                print(f"    var \\{param_name}")
            else:
                raise Exception("Unknown argument type " + arg)
        print(".endm")
        print()

    def emit_env_cmd(op, cmd):
        mn = cmd[0]
        param_list = []
        for i, arg in enumerate(cmd[1:]):
            param_list.append(chr(97 + i))
        print(f".macro envelope_{mn} {', '.join(param_list)}".rstrip())
        if op is not None:
            print(f"    .byte {hex(op >> 8)}, {hex(op & 0xff)}")
        for param in param_list:
            print_hword("\\" + param)
        print(".endm\n")

    for key in ['seq', 'chan', 'layer']:
        print(f"# {key} commands\n")
        if key == 'layer':
            cmds = commands['layer_large']
            for op in sorted(commands['layer_small'].keys()):
                if op not in cmds:
                    emit_cmd(key, op, commands['layer_small'][op])
        else:
            cmds = commands[key]
        eu = []
        non_eu = []
        for op in sorted(cmds.keys()):
            mn = cmds[op][0]
            if mn == 'setnotepriority':
                eu.append((0xe9, ['setnotepriority', 'u8']))
                non_eu.append((op, cmds[op]))
            elif mn in ['reservenotes', 'unreservenotes']:
                eu.append((op - 1, cmds[op]))
                non_eu.append((op, cmds[op]))
            elif mn not in ['portamento', 'writeseq']:
                emit_cmd(key, op, cmds[op])

        if key == 'chan':
            print(".macro chan_writeseq val, pos, offset")
            print("    .byte 0xc7, \\val")
            print_hword("(\\pos - sequence_start + \\offset)")
            print(".endm\n")
            print(".macro chan_writeseq_nextinstr val, offset")
            print("    .byte 0xc7, \\val")
            print_hword("(writeseq\\@ - sequence_start + \\offset)")
            print("    writeseq\\@:")
            print(".endm\n")
            print(".macro layer_portamento a, b, c")
            print("    .byte 0xc7, \\a, \\b")
            print("    .if ((\\a & 0x80) == 0)")
            print("        var \\c")
            print("    .else")
            print("        .byte \\c")
            print("    .endif")
            print(".endm\n")
            emit_cmd(key, 0xfd, ['delay_long', 'var_long'])
        if key == 'layer':
            emit_cmd(key, 0xc0, ['delay_long', 'var_long'])
            emit_cmd(key, 0x40, ['note1_long', 'arg', 'var_long', 'u8'])
        if eu:
            print(".ifdef VERSION_EU\n")
            for (op, cmd) in eu:
                emit_cmd(key, op, cmd)
            print(".else\n")
            for (op, cmd) in non_eu:
                emit_cmd(key, op, cmd)
            print(".endif\n")

    print("# envelope commands\n")
    emit_env_cmd(0, ['disable', 'u16'])
    emit_env_cmd(2**16-1, ['hang', 'u16'])
    emit_env_cmd(2**16-2, ['goto', 'u16'])
    emit_env_cmd(2**16-3, ['restart', 'u16'])
    emit_env_cmd(None, ['line', 'u16', 'u16'])

    print("# other commands\n")
    print(".macro var_long x")
    print("     .byte (0x80 | (\\x & 0x7f00) >> 8), (\\x & 0xff)")
    print(".endm\n")
    print(".macro var x")
    print("    .if (\\x >= 0x80)")
    print("        var_long \\x")
    print("    .else")
    print("        .byte \\x")
    print("    .endif")
    print(".endm\n")
    print(".macro sound_ref a")
    print_hword("(\\a - sequence_start)")
    print(".endm")
    sys.exit(0)

filename = sys.argv[1]
try:
    lang = filename.split('/')[-2]
    assert lang in ['us', 'jp', 'eu']
    seq_num = int(filename.split('/')[-1].split('_')[0], 16)
except Exception:
    lang = ''
    seq_num = -1

try:
    with open(filename, 'rb') as f:
        data = f.read()
except Exception:
    print("Error: could not open file {filename} for reading.", file=sys.stderr)
    sys.exit(1)

output = [None] * len(data)
output_instate = [None] * len(data)
label_name = [None] * len(data)
script_start = [False] * len(data)
hit_eof = False
errors = []
seq_writes = []

# Our analysis of large notes mode doesn't persist through multiple channel activations
# For simplicity, we force large notes always instead, which is valid for SM64.
force_large_notes = True

if lang == 'eu':
    # unreservenotes moved to 0xf0 in EU, and reservenotes took its place
    commands['chan'][0xf0] = commands['chan'][0xf1]
    commands['chan'][0xf1] = commands['chan'][0xf2]
    del commands['chan'][0xf2]
    # total guess: the same is true for the 'seq'-type command
    commands['seq'][0xf0] = commands['seq'][0xf1]
    commands['seq'][0xf1] = commands['seq'][0xf2]
    del commands['seq'][0xf2]
    # setnotepriority moved to 0xe9, becoming a non-arg command
    commands['chan'][0xe9] = ['setnotepriority', 'u8']
    del commands['chan'][0x60]

def is_arg_command(cmd_args):
    return 'arg' in cmd_args or 'ign-arg' in cmd_args

def gen_label(ind, tp):
    nice_tp = tp.replace('_small', '').replace('_large', '')
    addr = hex(ind)[2:].upper()
    ret = f".{nice_tp}_{addr}"
    if ind >= len(data):
        errors.append(f"reference to oob label {ret}")
        return ret

    if label_name[ind] is not None:
        return label_name[ind]
    label_name[ind] = ret
    return ret

def gen_mnemonic(tp, b):
    nice_tp = tp.split('_')[0]
    mn = commands[tp][b][0]
    if not mn:
        mn = f"{b:02X}"
    return f"{nice_tp}_{mn}"

decode_list = []

def decode_one(state):
    pos, tp, nesting, large = state
    orig_pos = pos

    if pos >= len(data):
        global hit_eof
        hit_eof = True
        return

    if output[pos] is not None:
        if output_instate[pos] != state:
            errors.append(f"got to {gen_label(orig_pos, tp)} with both state {state} and {output_instate[pos]}")
        return

    def u8():
        nonlocal pos
        global hit_eof
        if pos == len(data):
            hit_eof = True
            return 0
        ret = data[pos]
        pos += 1
        return ret

    def u16():
        hi = u8()
        lo = u8()
        return (hi << 8) | lo

    def var():
        ret = u8()
        if ret & 0x80:
            ret = (ret << 8) & 0x7f00;
            ret |= u8()
            return (ret, ret < 0x80)
        return (ret, False)

    if tp == 'soundref':
        sound = u16()
        decode_list.append((sound, 'chan', 0, True))
        if sound < len(data):
            script_start[sound] = True
        for p in range(orig_pos, pos):
            output[p] = ''
            output_instate[p] = state
        output[orig_pos] = 'sound_ref ' + gen_label(sound, 'chan')
        return

    if tp == 'envelope':
        a = u16()
        b = u16()
        for p in range(orig_pos, pos):
            output[p] = ''
            output_instate[p] = state
        if a >= 2**16 - 3:
            a -= 2**16
        if a <= 0:
            mn = ['disable', 'hang', 'goto', 'restart'][-a]
            output[orig_pos] = f'envelope_{mn} {b}'
            # assume any goto is backwards and stop decoding
        else:
            output[orig_pos] = f'envelope_line {a} {b}'
            decode_list.append((pos, tp, nesting, large))
        return

    ins_byte = u8()

    cmds = commands[tp]
    if ins_byte in cmds and not is_arg_command(cmds[ins_byte]):
        used_b = ins_byte
        arg = None
    elif ins_byte & 0xf0 in cmds and is_arg_command(cmds[ins_byte & 0xf0]):
        used_b = ins_byte & 0xf0
        arg = ins_byte & 0xf
    elif ins_byte & 0xc0 in cmds and is_arg_command(cmds[ins_byte & 0xc0]) and tp.startswith('layer'):
        used_b = ins_byte & 0xc0
        arg = ins_byte & 0x3f
    else:
        errors.append(f"unrecognized instruction {hex(ins_byte)} for type {tp} at label {gen_label(orig_pos, tp)}")
        return

    out_mn = gen_mnemonic(tp, used_b)
    out_args = []
    cmd_mn = cmds[used_b][0]
    cmd_args = cmds[used_b][1:]
    long_var = False

    for a in cmd_args:
        if cmd_mn == 'portamento' and len(out_args) == 2 and (int(out_args[0], 0) & 0x80) == 0:
            a = 'var'
        if a == 'arg':
            out_args.append(str(arg))
        elif a == 'ign-arg' and arg != 0:
            out_args.append(str(arg))
        elif a == 'u8':
            out_args.append(str(u8()))
        elif a == 'hex8':
            out_args.append(hex(u8()))
        elif a == 's8':
            v = u8()
            out_args.append(str(v if v < 128 else v - 256))
        elif a == 'u16':
            out_args.append(str(u16()))
        elif a == 'hex16':
            out_args.append(hex(u16()))
        elif a == 'var':
            val, bad = var()
            out_args.append(hex(val))
            if bad:
                long_var = True
        elif a == 'addr':
            v = u16()

            kind = 'addr'
            if cmd_mn == 'call':
                kind = tp + '_fn'
            elif cmd_mn in ['jump', 'beqz', 'bltz', 'bgez']:
                kind = tp
            elif cmd_mn == 'startchannel':
                kind = 'chan'
            elif cmd_mn == 'setlayer':
                kind = 'layer'
            elif cmd_mn == 'setdyntable':
                kind = 'table'
            elif cmd_mn == 'setenvelope':
                kind = 'envelope'

            if v >= len(data):
                label = gen_label(v, kind)
                out_args.append(label)
                errors.append(f"reference to oob label {label}")
            elif cmd_mn == 'writeseq':
                out_args.append('<fixup>')
                seq_writes.append((orig_pos, v))
            else:
                out_args.append(gen_label(v, kind))
                if cmd_mn == 'call':
                    decode_list.append((v, tp, 0, large))
                    script_start[v] = True
                elif cmd_mn in ['jump', 'beqz', 'bltz', 'bgez']:
                    decode_list.append((v, tp, nesting, large))
                elif cmd_mn == 'startchannel':
                    decode_list.append((v, 'chan', 0, force_large_notes))
                    script_start[v] = True
                elif cmd_mn == 'setlayer':
                    if large:
                        decode_list.append((v, 'layer_large', 0, True))
                    else:
                        decode_list.append((v, 'layer_small', 0, True))
                    script_start[v] = True
                elif cmd_mn == 'setenvelope':
                    decode_list.append((v, 'envelope', 0, True))
                    script_start[v] = True
                else:
                    script_start[v] = True

    out_all = out_mn
    if long_var:
        out_all += "_long"
    if out_args:
        out_all += ' '
        out_all += ', '.join(out_args)
    for p in range(orig_pos, pos):
        output[p] = ''
        output_instate[p] = state
    output[orig_pos] = out_all

    if cmd_mn in ['hang', 'jump']:
        return
    if cmd_mn in ['loop']:
        nesting += 1
    if cmd_mn == 'end':
        nesting -= 1
    if cmd_mn in ['break', 'loopend']:
        nesting -= 1
        if nesting < 0:
            # This is iffy, and actually happens in sequence 0. It will make us
            # return to the caller's caller at function end.
            nesting = 0
    if cmd_mn == 'largenoteson':
        large = True
    if cmd_mn == 'largenotesoff':
        large = False
    if nesting >= 0:
        decode_list.append((pos, tp, nesting, large))

def decode_rec(state, initial):
    if not initial:
        v = state[0]
        gen_label(v, state[1])
        script_start[v] = True
    decode_list.append(state)
    while decode_list:
        decode_one(decode_list.pop())

def main():
    decode_rec((0, 'seq', 0, False), initial=True)

    if seq_num == 0:
        if lang == 'jp':
            sound_banks = [
                (0x14C, 0x70),
                (0x8A8, 0x38), # stated as 0x30
                (0xB66, 0x38), # stated as 0x30
                (0xE09, 0x80),
                (0x194B, 0x28), # stated as 0x20
                (0x1CA6, 0x80),
                (0x27C9, 0x20),
                (0x2975, 0x30),
                # same script as bank 3
                # same script as bank 5
            ]
            unused = [
                (0x1FC4, 'layer_large'),
                (0x2149, 'layer_large'),
                (0x2223, 'layer_large'),
                (0x28C5, 'chan'),
                (0x3110, 'envelope'),
                (0x31EC, 'envelope'),
            ]
        elif lang == 'us':
            sound_banks = [
                (0x14C, 0x70),
                (0x8F6, 0x38), # stated as 0x30
                (0xBB4, 0x40),
                (0xF8E, 0x80),
                (0x1AF3, 0x28), # stated as 0x20
                (0x1E4E, 0x80),
                (0x2971, 0x20),
                (0x2B1D, 0x40),
                # same script as bank 3
                # same script as bank 5
            ]
            unused = [
                (0x216C, 'layer_large'),
                (0x22F1, 'layer_large'),
                (0x23CB, 'layer_large'),
                (0x2A6D, 'chan'),
                (0x339C, 'envelope'),
                (0x3478, 'envelope'),
            ]
        elif lang == 'eu':
            sound_banks = [
                (0x154, 0x70),
                (0x8FE, 0x38), # stated as 0x30?
                (0xBBC, 0x40),
                (0xFA5, 0x80),
                (0x1B0C, 0x28), # stated as 0x20?
                (0x1E67, 0x80),
                (0x298A, 0x20),
                (0x2B36, 0x40),
                # same script as bank 3
                # same script as bank 5
            ]
            unused = [
                (0xF9A, 'chan'),
                (0x2185, 'layer_large'),
                (0x230A, 'layer_large'),
                (0x23E4, 'layer_large'),
                (0x2A86, 'chan'),
                (0x33CC, 'envelope'),
                (0x34A8, 'envelope'),
            ]

        for (addr, count) in sound_banks:
            for i in range(count):
                decode_rec((addr + 2*i, 'soundref', 0, False), initial=True)

        for (addr, tp) in unused:
            gen_label(addr, tp + '_unused')
            decode_rec((addr, tp, 0, force_large_notes), initial=False)

    for (pos, write_to) in seq_writes:
        assert '<fixup>' in output[pos]
        delta = 0
        while output[write_to] == '':
            write_to -= 1
            delta += 1
        if write_to > pos and all(output[i] == '' for i in range(pos+1, write_to)):
            nice_target = str(delta)
            output[pos] = output[pos].replace('writeseq', 'writeseq_nextinstr')
        else:
            tp = output_instate[write_to][1] if output_instate[write_to] is not None else 'addr'
            nice_target = gen_label(write_to, tp) + ", " + str(delta)
        output[pos] = output[pos].replace('<fixup>', nice_target)

    # Add unreachable 'end' markers
    for i in range(1, len(data)):
        if (data[i] == 0xff and output[i] is None and output[i - 1] is not None
                and label_name[i] is None):
            tp = output_instate[i - 1][1]
            if tp in ["seq", "chan", "layer_small", "layer_large"]:
                output[i] = gen_mnemonic(tp, 0xff)

    # Add envelope padding
    for i in range(1, len(data) - 1):
        if (data[i] == 0 and output[i] is None and output[i - 1] is not None and
                output[i + 1] is not None and label_name[i] is None and
                output[i + 1].startswith('envelope')):
            script_start[i] = True
            output[i] = "# padding\n.byte 0"

    # Add 'unused' marker labels
    for i in range(1, len(data)):
        if (output[i] is None and output[i - 1] is not None and label_name[i] is None):
            script_start[i] = True
            gen_label(i, 'unused')

    # Remove up to 15 bytes of padding at the end
    end_padding = 0
    for i in range(len(data)-1, -1, -1):
        if output[i] is not None:
            break
        end_padding += 1
    if end_padding > 15:
        end_padding = 0

    if print_end_padding:
        print(end_padding)
        sys.exit(0)

    print(".include \"seq_macros.inc\"")
    print(".section .rodata")
    print(".align 0")
    print("sequence_start:")
    print()
    for i in range(len(data) - end_padding):
        if script_start[i] and i > 0:
            print()
        if label_name[i] is not None:
            print(f"{label_name[i]}:")
        o = output[i]
        if o is None:
            print(f".byte {hex(data[i])}")
        elif o:
            print(o)
        elif label_name[i] is not None:
            print("<mid-instruction>")
            errors.append(f"mid-instruction label {label_name[i]}")

    if hit_eof:
        errors.append("hit eof!?")

    if errors:
        print(f"[{filename}] errors:", file=sys.stderr)
        for w in errors:
            print(w, file=sys.stderr)

main()
