bl_info = {
    "name": "Piano MIDI Visualizer com Animação",
    "author": "ChatGPT",
    "version": (1, 1, 0),
    "blender": (2, 80, 0),
    "description": "Visualiza e anima tecla do piano ao receber notas MIDI (com suporte a coleção customizada)",
    "category": "3D View",
}

import bpy
import mido
import threading
import time
from bpy.props import EnumProperty, PointerProperty

# Globals
midi_in = None
midi_thread = None
thread_running = False
active_notes = set()
lock = threading.Lock()
start_note = 21      # Nota MIDI inicial A0
timer_registered = False
KEY_PRESS_OFFSET = -0.05

# --- Criar piano padrão com cubos ---
def create_piano():
    # Remove piano antigo
    for obj in list(bpy.data.objects):
        if obj.name.startswith("PianoKey_"):
            bpy.data.objects.remove(obj, do_unlink=True)

    key_width = 0.22

    mat_white = bpy.data.materials.get("WhiteKey") or bpy.data.materials.new("WhiteKey")
    mat_red = bpy.data.materials.get("RedKey") or bpy.data.materials.new("RedKey")

    for mat, color in [(mat_white, (1, 1, 1, 1)), (mat_red, (1, 0, 0, 1))]:
        mat.use_nodes = True
        principled = mat.node_tree.nodes.get("Principled BSDF")
        if principled:
            principled.inputs["Base Color"].default_value = color

    for i in range(88):
        note = start_note + i
        bpy.ops.mesh.primitive_cube_add(size=1)
        key = bpy.context.active_object
        key.name = f"PianoKey_{note}"
        key.scale = (key_width / 2, 0.7 / 2, 0.1 / 2)
        key.location = (i * key_width, 0, 0)

        if key.data.materials:
            key.data.materials[0] = mat_white
        else:
            key.data.materials.append(mat_white)

        key["init_z"] = key.location.z


# --- Thread escutando mensagens MIDI ---
def midi_listener(port_name):
    global midi_in, thread_running, active_notes, lock

    try:
        midi_in = mido.open_input(port_name)
        print(f"[MIDI] Porta '{port_name}' aberta.")
    except Exception as e:
        print(f"[MIDI] Erro ao abrir porta: {e}")
        with lock:
            thread_running = False
        return

    while True:
        with lock:
            if not thread_running:
                break
        for msg in midi_in.iter_pending():
            with lock:
                if msg.type == 'note_on' and msg.velocity > 0:
                    active_notes.add(msg.note)
                elif msg.type == 'note_off' or (msg.type == 'note_on' and msg.velocity == 0):
                    active_notes.discard(msg.note)
        time.sleep(0.01)

    midi_in.close()
    print("[MIDI] Thread finalizada e porta fechada.")


# --- Função para atualizar teclas ---
def update_keys():
    global active_notes, lock, thread_running, timer_registered

    props = bpy.context.scene.midipiano_props

    piano_col = props.piano_collection

    # Se coleção configurada, usar objetos da coleção
    if piano_col:
        # Assume que as teclas estão nomeadas no formato: "Key_<nota MIDI>", ex: "Key_60"
        for i in range(88):
            note = start_note + i
            key_name = f"Key_{note}"
            key = piano_col.objects.get(key_name)
            if key is None:
                continue

            with lock:
                pressed = note in active_notes

            # Guardar z init como custom property para o objeto, se não existir
            if "init_z" not in key:
                key["init_z"] = key.location.z
            z_init = key["init_z"]
            z_target = z_init + KEY_PRESS_OFFSET if pressed else z_init

            key.location.z = z_target

            # Alterar cor do material (se material e principled existirem)
            if key.data.materials:
                mat = key.data.materials[0]
                if mat.use_nodes:
                    principled = mat.node_tree.nodes.get("Principled BSDF")
                    if principled:
                        color = (1, 0, 0, 1) if pressed else (1, 1, 1, 1)
                        principled.inputs["Base Color"].default_value = color
    else:
        # Sem coleção - usar comportamento padrão, teclas criadas com cubos
        for i in range(88):
            note = start_note + i
            key = bpy.data.objects.get(f"PianoKey_{note}")
            if key is None:
                continue

            with lock:
                pressed = note in active_notes

            if "init_z" not in key:
                key["init_z"] = key.location.z
            z_init = key["init_z"]
            z_target = z_init + KEY_PRESS_OFFSET if pressed else z_init

            key.location.z = z_target

            if key.data.materials:
                mat = key.data.materials[0]
                if mat.use_nodes:
                    principled = mat.node_tree.nodes.get("Principled BSDF")
                    if principled:
                        color = (1, 0, 0, 1) if pressed else (1, 1, 1, 1)
                        principled.inputs["Base Color"].default_value = color

    if thread_running:
        return 0.05
    else:
        timer_registered = False
        return None


# --- Enum para portas MIDI ---
def get_midi_ports_enum(self, context):
    ports = mido.get_input_names()
    if not ports:
        return [("NONE", "Nenhuma Porta MIDI", "")]
    return [(p, p, "") for p in ports]


# --- Properties ---
class MIDIPianoProps(bpy.types.PropertyGroup):
    midi_ports: EnumProperty(
        name="Porta MIDI",
        description="Selecione a porta MIDI",
        items=get_midi_ports_enum,
    )
    piano_collection: PointerProperty(
        name="Coleção Piano",
        type=bpy.types.Collection,
        description="Selecione a coleção contendo as teclas do seu modelo 3D de piano",
    )


# --- Operador Start ---
class MIDIPiano_OT_Start(bpy.types.Operator):
    bl_idname = "midipiano.start"
    bl_label = "Iniciar Escuta MIDI"
    bl_description = "Inicia a leitura MIDI em tempo real com animação"

    def execute(self, context):
        global midi_thread, thread_running, timer_registered

        props = context.scene.midipiano_props
        port_name = props.midi_ports
        if port_name == "NONE":
            self.report({'ERROR'}, "Nenhuma porta MIDI disponível.")
            return {'CANCELLED'}

        with lock:
            if thread_running:
                self.report({'WARNING'}, "Escuta MIDI já está rodando.")
                return {'CANCELLED'}

            # Se não tiver coleção selecionada, cria cubos default
            if not props.piano_collection:
                create_piano()

            thread_running = True
            midi_thread = threading.Thread(target=midi_listener, args=(port_name,), daemon=True)
            midi_thread.start()

        if not timer_registered:
            bpy.app.timers.register(update_keys)
            timer_registered = True

        self.report({'INFO'}, f"Escuta MIDI iniciada na porta '{port_name}'.")
        return {'FINISHED'}


# --- Operador Stop ---
class MIDIPiano_OT_Stop(bpy.types.Operator):
    bl_idname = "midipiano.stop"
    bl_label = "Parar Escuta MIDI"
    bl_description = "Para a leitura MIDI e remove o piano"

    def execute(self, context):
        global thread_running, active_notes, timer_registered

        with lock:
            if not thread_running:
                self.report({'WARNING'}, "Escuta MIDI não está rodando.")
                return {'CANCELLED'}

            thread_running = False
            active_notes.clear()

        # Quando thread_running for False, timer cancela a si mesmo

        # Remove cubos criados se coleção não estiver selecionada
        props = context.scene.midipiano_props
        if not props.piano_collection:
            for obj in list(bpy.data.objects):
                if obj.name.startswith("PianoKey_"):
                    bpy.data.objects.remove(obj, do_unlink=True)

        timer_registered = False

        self.report({'INFO'}, "Escuta MIDI parada e piano removido.")
        return {'FINISHED'}


# --- UI Panel ---
class MIDIPiano_PT_Panel(bpy.types.Panel):
    bl_label = "Piano MIDI com Animação"
    bl_idname = "MIDIPIANO_PT_panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "MIDI Piano"

    def draw(self, context):
        layout = self.layout
        props = context.scene.midipiano_props

        ports = mido.get_input_names()

        if not ports:
            layout.label(text="Nenhuma porta MIDI encontrada", icon='ERROR')
            return

        layout.prop(props, "midi_ports")
        layout.prop(props, "piano_collection")

        row = layout.row()
        row.operator("midipiano.start", icon='PLAY')
        row.operator("midipiano.stop", icon='PAUSE')


# --- Registro ---
classes = (
    MIDIPianoProps,
    MIDIPiano_OT_Start,
    MIDIPiano_OT_Stop,
    MIDIPiano_PT_Panel,
)

def register():
    for c in classes:
        bpy.utils.register_class(c)
    bpy.types.Scene.midipiano_props = PointerProperty(type=MIDIPianoProps)
    print("Addon 'Piano MIDI com Animação' registrado!")

def unregister():
    for c in reversed(classes):
        bpy.utils.unregister_class(c)
    del bpy.types.Scene.midipiano_props
    print("Addon 'Piano MIDI com Animação' desregistrado!")


if __name__ == "__main__":
    register()