package kr.gui.metrodome;

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeListener;
import java.sql.DriverManager;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.KeyStroke;

import kr.gui.GuiModel;
import kr.gui.TimeSyncedController;
import kr.gui.midigraph.MidiGraph;
import kr.gui.midigraph.MidiTuneGraph;
import kr.gui.midigraph.MidiTuneGraph.MousePositionStatus;
import kr.gui.util.DefaultUIMode;
import kr.gui.util.FrozenUIMode;
import kr.gui.util.MouseInputUIMode;
import kr.gui.util.UIMode;
import kr.gui.util.UIModeHandler;
import kr.miditunemodel.ChangeMetrodomeOperation;
import kr.miditunemodel.ChangeNoteOperation;
import kr.miditunemodel.CreateEventOperation;
import kr.miditunemodel.Event;
import kr.miditunemodel.EventGroup;
import kr.miditunemodel.EventGuiModel;
import kr.miditunemodel.MetrodomeEvent;
import kr.miditunemodel.MetrodomeEventTransaction;
import kr.miditunemodel.MidiNoteTransaction;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEvent;
import kr.miditunemodel.NoteEventGuiModel;
import kr.util.Listener;
import kr.util.ListenerManager;

public class MetrodomeGraphController extends TimeSyncedController implements Listener
{
	private MetrodomeGuiModel eventGuiModel;
	private MetrodomeGraph graph;
	
	private MidiTuneModel midiTuneModel;
	private GuiModel guiModel;
	public UIModeHandler uiModeHandler;
	
	public MetrodomeGraphController()
	{
	}
	
	/**
	 * Removes the last mode from the stack and removes it as
	 * a listener.
	 */
	public void init(GuiModel guiModel, MidiTuneModel midiModel, MetrodomeGraph graph, MetrodomeGuiModel eventGuiModel)
	{
		this.guiModel = guiModel;
		this.eventGuiModel = eventGuiModel;
		this.midiTuneModel = midiModel;
		this.graph = graph;

		uiModeHandler = new UIModeHandler(graph);
		uiModeHandler.addMode(new NormalUIMode());
		super.init(guiModel, graph);
		ListenerManager.inst().registerListener(midiModel.getMetrodomeEventGroup(), this);
		ListenerManager.inst().registerListener(eventGuiModel, this);
		ListenerManager.inst().registerListener(guiModel, this);
		
		graph.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(
                KeyEvent.VK_B, 0),"beat");
		graph.getActionMap().put("beat", new AbstractAction()
				{
			public void actionPerformed(java.awt.event.ActionEvent e) 
			{
				if(MetrodomeGraphController.this.guiModel.isPlaying())
					MetrodomeGraphController.this.eventGuiModel.addBeat(MetrodomeGraphController.this.guiModel.getNowMicros());
			}
				});
		

		graph.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(
                KeyEvent.VK_C, 0),"clear");
		graph.getActionMap().put("clear", new AbstractAction()
				{
			public void actionPerformed(java.awt.event.ActionEvent e) 
			{
				System.out.println("cleared");
				MetrodomeGraphController.this.eventGuiModel.clearBeats();
				MetrodomeGraphController.this.graph.repaintScrollingData();
			}
				});
		

	}
	
	public void notify(Object source, Object type, Object... values) {
		if(source == guiModel)
		{
			if(type == GuiModel.MessageType.IS_PLAYING_UPDATED)
			{
				if(guiModel.isPlaying())
					uiModeHandler.replaceMode(new FrozenUIMode());
				else
					uiModeHandler.replaceMode(new NormalUIMode());
				
				graph.repaintScrollingData();
			}
		}
		else if(source == midiTuneModel.getMetrodomeEventGroup())
		{
			if(type ==  EventGroup.MessageType.EVENTS_CHANGED)
			{
				//PERF Don't repaint the whole screen, just redraw the note
				graph.repaintScrollingData();
			}
		}
		else if(source == eventGuiModel)
		{
			if(type ==  EventGuiModel.MessageType.SELECTION_CHANGED)
			{
				//PERF Don't repaint the whole screen, just redraw the affected notes
				// also need to make note event gui model return the affected notes
				graph.repaintScrollingData();
			}
		}
	}
	
	/**
	 * Normal mode when no buttons are held down
	 */
	private class NormalUIMode extends DefaultUIMode
	{
		private static final int MAX_PIXELS_FROM_MARK = 3;

		public NormalUIMode() {
			super((EventGuiModel)eventGuiModel, (MidiGraph)graph, (MidiTuneModel)midiTuneModel, 
					(EventGroup)midiTuneModel.getMetrodomeEventGroup());
		}

		public void mousePressed(MouseEvent e)
		{
			graph.requestFocus();
			if(e.getButton()!=e.BUTTON1)
			{
				MetrodomeEventTransaction temp = new MetrodomeEventTransaction(midiTuneModel);

				CreateEventOperation<MetrodomeEvent> op = new CreateEventOperation<MetrodomeEvent>(midiTuneModel.getMetrodomeEventGroup());
				
				MetrodomeEvent me = new MetrodomeEvent(graph.pixelsToMicros(e.getX()), MidiTuneModel.STANDARD_MICROS_PER_QUARTER_NOTE);
				op.setNewEvent(me);
				
				temp.add(op);
				
				temp.doIt();
				
				eventGuiModel.clearSelectedEvents();
				eventGuiModel.setActiveEvent(me);
				eventGuiModel.addSelectedEvent(eventGuiModel.getActiveEvent());
				
				return;
			}
			
			//
			//determine which event was selected and what operation to perform
			//
			MetrodomeEvent me = graph.getEvent(e.getX(), e.getY());
			if(me==null)
				throw new IllegalStateException("can't have a null MetrodomeEvent");
			
			MetrodomeEvent selectedEvent = null;
			
			boolean moveMetrodomeEvent = false;
			
			long microsFromEventStart = graph.pixelsToMicros(e.getX())- me.getMicros();
			
			long divLength = graph.getDivisionLength(me.getQuarterNoteMicros());
			
			long microsFromMark = microsFromEventStart % divLength;
						
			if(microsFromMark > divLength/2)
				microsFromMark = divLength - microsFromMark;

			if(MAX_PIXELS_FROM_MARK > graph.microsToRelPixels(microsFromEventStart))
			{
				moveMetrodomeEvent = true;
				selectedEvent = me;
			}
			else if (MAX_PIXELS_FROM_MARK > graph.microsToRelPixels(microsFromMark)){
				moveMetrodomeEvent = false;
				selectedEvent = me;
			}
			else {
				me = (MetrodomeEvent) me.getNext();
				if(me != null && graph.microsToPixels(me.getMicros()) - e.getX() < MAX_PIXELS_FROM_MARK)
				{
					moveMetrodomeEvent = true;
					selectedEvent = me;				
				}
			}
			
			//
			// perform necessary actions
			//
			if(selectedEvent != null)
			{
				super.mousePressed(e, me);
				
				if(moveMetrodomeEvent)
					uiModeHandler.addMode(new MoveMetrodomeUIMode(e.getX()));
				else
					uiModeHandler.addMode(new ChangeBeatUIMode(e.getX()));
			}
		}
	}

	private class MoveMetrodomeUIMode extends MouseInputUIMode
	{
		private long currMicros;

		MoveMetrodomeUIMode(int currX)
		{
			this.currMicros = graph.pixelsToMicros(currX);
		}
		public void mouseReleased(MouseEvent e) {
			//this mode finishes when the mouse is released
			uiModeHandler.removeMode(this);
		}

		public void mouseDragged(MouseEvent e) {
			long newMicros = graph.pixelsToMicros(e.getX());
			long microsDelta = newMicros - currMicros;

			MetrodomeEventTransaction temp = new MetrodomeEventTransaction(midiTuneModel);

			ChangeMetrodomeOperation op = new ChangeMetrodomeOperation(eventGuiModel.getActiveEvent());
			
			op.setMicrosDelta(microsDelta);
			
			temp.add(op);
			
			temp.doIt();
			
			currMicros = newMicros;
		}
	}

	
	private class ChangeBeatUIMode extends MouseInputUIMode
	{
		private long startMicros;
		private long startMicrosPerQtrNote;

		ChangeBeatUIMode(int currX)
		{
			this.startMicros = graph.pixelsToMicros(currX);
			this.startMicrosPerQtrNote = eventGuiModel.getActiveEvent().getQuarterNoteMicros();
		}
		public void mouseReleased(MouseEvent e) {
			//this mode finishes when the mouse is released
			uiModeHandler.removeMode(this);
		}

		public void mouseDragged(MouseEvent e) {
			long newMicrosPerQtrNote = 
				Math.round(1. * (graph.pixelsToMicros(e.getX()) - eventGuiModel.getActiveEvent().getMicros()) /
				(startMicros - eventGuiModel.getActiveEvent().getMicros()) * startMicrosPerQtrNote);

			MetrodomeEventTransaction temp = new MetrodomeEventTransaction(midiTuneModel);

			ChangeMetrodomeOperation op = new ChangeMetrodomeOperation(eventGuiModel.getActiveEvent());
			
			op.setNewQuarterNoteMicros(newMicrosPerQtrNote);
			
			temp.add(op);
			
			temp.doIt();
		}
	}


}
