from ableton.v2.base import listens
from ableton.v2.control_surface.component import Component

from ..util import track_id

class NotesModeComponent(Component):
	"A component that handles the Notes mode of the Exquis, and that displays the notes of the currently playing clip"

	def __init__(self, *a, **k):
		super(NotesModeComponent, self).__init__(*a, **k)
		self.target_track = None
		self.target_clip = None
		self.keeper = NoteKeeper(self)
		self.last_playing_position = 0
		self._on_is_playing_changed.subject = self.song
		self._on_visible_tracks_changed.subject = self.song
		self._on_selected_track_changed.subject = self.song.view

	def update(self):
		self._update_target_track()
		self.keeper.resume_ongoing_notes = True

	@listens("is_playing")
	def _on_is_playing_changed(self):
		self._update_target_track()

	@listens("visible_tracks")
	def _on_visible_tracks_changed(self):
		self._update_target_track()
	
	@listens("selected_track")
	def _on_selected_track_changed(self):
		self._update_target_track()

	def _update_target_track(self):
		target = None
		if self.song.is_playing:
			explicit_track_id = self.song.get_data("exquis_explicit_note_monitor_track", None)
			if explicit_track_id:
				for track in self.song.tracks:
					if track_id(track) == explicit_track_id:
						target = track
						break
		
		self.target_track = target
		self._on_playing_slot_index_changed.subject = target
		self.keeper.reset()
		self._update_playing_clip()

	@listens("playing_slot_index")
	def _on_playing_slot_index_changed(self):
		self._update_playing_clip()

	def _update_playing_clip(self):
		if not self.target_track:
			self.target_clip = None
			self.keeper.reset()
			return
		
		clip = None
		if self.target_track.playing_slot_index >= 0:
			slot = self.target_track.clip_slots[self.target_track.playing_slot_index]
			if slot.has_clip and slot.clip.is_midi_clip:
				clip = slot.clip
		
		if clip != self.target_clip or (clip is None and self.target_clip is not None):
			self.target_clip = clip
			self._on_playing_position_changed.subject = clip

		self.keeper.reset()
		self._process_monitoring()

	@listens("playing_position")
	def _on_playing_position_changed(self):
		self._process_monitoring()
	
	def _process_monitoring(self):
		if not self.target_clip:
			self.keeper.reset()
			return

		position = self.target_clip.playing_position

		self.last_playing_position = position
		margin = 0.05  # Small backward margin to catch early notes
		start = max(0, position - margin)
		notes = self.target_clip.get_notes(start, 0, 0.1 + margin, 128)
		#notes = self.target_clip.get_notes(position, 0, 0.1, 128)
		self.keeper.process(notes, position)
			
class NoteKeeper:
	"Keeps track of the notes that are currently playing in a clip and sends MIDI notes to the controller"

	def __init__(self, parent):
		self.parent = parent
		self.notes = { i: 0 for i in range(128) }
		self.resume_ongoing_notes = False
		
	def reset(self):
		for k, v in self.notes.items():
			if v:
				self._send(k, False)
				self.notes[k] = 0

	def process(self, notes, position):
		if self.resume_ongoing_notes:
			self.resume_ongoing_notes = False
			self._resume_ongoing_notes()
		
		for note in notes:
			i = note[0]
			time = note[1]
			duration = note[2]
			time_end = time + duration
			if time_end > self.notes[i]:
				self.notes[i] = time_end
				self._send(i, True)
		
		for k, v in self.notes.items():
			if v and v < position:
				self.notes[k] = 0
				self._send(k, False)
	
	def _resume_ongoing_notes(self):
		clip = self.parent.target_clip
		if not clip:
			return
		
		try:
			notes = clip.get_notes(-1, 0, clip.playing_position, 
			128)
		except:
			notes = []
		
		for note in notes:
			i = note[0]
			time = note[1]
			duration = note[2]
			time_end = time + duration
			if time_end > clip.playing_position:
				self.notes[i] = time_end
				self._send(i, True)

	def _send_midi(self, value):
		if self.parent.canonical_parent:
			self.parent.canonical_parent._send_midi(value)

	def _send(self, note, state):
		self._send_midi((0x90 if state else 0x80, note, 127 if state else 0))
