package kr.gui.midigraph;

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.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import kr.AudioModel;
import kr.gui.GuiModel;
import kr.gui.TimeSyncedController;
import kr.gui.midigraph.MidiTuneGraph.MousePositionStatus;
import kr.gui.util.MouseInputUIMode;
import kr.gui.util.UIMode;
import kr.gui.util.UIModeHandler;
import kr.miditunemodel.ChangeNoteOperation;
import kr.miditunemodel.EventGroup;
import kr.miditunemodel.EventGuiModel;
import kr.miditunemodel.MetrodomeEvent;
import kr.miditunemodel.MidiNoteTransaction;
import kr.miditunemodel.MidiTransaction;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEvent;
import kr.miditunemodel.NoteEventGroup;
import kr.miditunemodel.NoteEventGuiModel;
import kr.util.Listener;
import kr.util.ListenerManager;

public class MidiTuneGraphController extends TimeSyncedController implements Listener
{
	private NoteEventGuiModel noteEventGuiModel;
	private MidiTuneGraph graph;
	
	private MidiTuneModel midiTuneModel;
	private GuiModel guiModel;
	public UIModeHandler uiModeHandler;

	private NoteEventGroup noteEventGroup;
	private AudioModel audioModel;
	private EventGroup<MetrodomeEvent> metEventGroup;

	public MidiTuneGraphController()
	{
	}
	
	/**
	 * Removes the last mode from the stack and removes it as
	 * a listener.
	 */
	public void init(GuiModel guiModel, NoteEventGuiModel noteEventGuiModel, MidiTuneModel midiModel, MidiTuneGraph graph,
			AudioModel audioModel)
	{
		this.guiModel = guiModel;
		this.noteEventGuiModel = noteEventGuiModel;
		this.midiTuneModel = midiModel;
		this.graph = graph;
		this.audioModel = audioModel;
		
		uiModeHandler = new UIModeHandler(graph);
		uiModeHandler.addMode(new NormalUIMode());
		super.init(guiModel, graph);
		this.noteEventGroup = midiModel.getNoteEventGroup();
		this.metEventGroup = midiModel.getMetrodomeEventGroup();
		ListenerManager.inst().registerListener(noteEventGroup, this);
		ListenerManager.inst().registerListener(metEventGroup, this);
		ListenerManager.inst().registerListener(noteEventGuiModel, this);
		ListenerManager.inst().registerListener(audioModel, this);
		ListenerManager.inst().registerListener(guiModel, this);
	}
	
	private void playNoteFromPosition(MouseEvent e)
	{
        int x = e.getX();
        int y = e.getY();
        
        noteEventGuiModel.playNote(graph.pixelsToNote(y));
	}
	
	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 PlayingUIMode());
				else
					uiModeHandler.replaceMode(new NormalUIMode());
			}
			else if(type == GuiModel.MessageType.VIEW_FFT_SCALE_UPDATED)
			{
				if(guiModel.isViewVocalsFFTFlag())
					graph.updateScale();
				else
					graph.clearScale();
			}	
		}
		else if(source == noteEventGroup)
		{
			if (type == EventGroup.MessageType.EVENTS_CHANGED) {
				//PERF Don't repaint the whole screen, just redraw the note
				graph.repaintScrollingData();
			}
		}
		else if(source == metEventGroup)
		{
			if (type == EventGroup.MessageType.EVENTS_CHANGED) {
				//PERF Don't repaint the whole screen, just redraw the note
				graph.repaintScrollingData();
			}
		}
		else if(source == noteEventGuiModel)
		{
			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();
			}
			else if(type ==  NoteEventGuiModel.MessageType.TEXT_UPDATED)
			{
				//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();
			}
		}
	}
	
	/**
	 * For moving either the start or end of a note
	 */
	private class MoveNoteEndUIMode extends MouseInputUIMode
	{
		private boolean moveStartFlag;
		private boolean adjustHeight;
		private NoteEvent firstNoteEvent;
		private NoteEvent lastNoteEvent;
		private List<NoteEvent> originalNotes = new ArrayList<NoteEvent>();
		private List<NoteEvent> modifiedNotes = new ArrayList<NoteEvent>();
		private NoteEvent activeEvent;

		public MoveNoteEndUIMode(boolean moveStartFlag, boolean adjustHeight, NoteEvent activeEvent) {
			this.moveStartFlag = moveStartFlag;
			this.adjustHeight = adjustHeight;
			
			for(NoteEvent ne : noteEventGuiModel.getSelectedEvents())
			{
				NoteEvent clone = ne.clone();
				originalNotes.add(clone);
				modifiedNotes.add(ne);
				if(firstNoteEvent == null || ne.getMicros() < firstNoteEvent.getMicros())
					firstNoteEvent = clone;
				if(lastNoteEvent == null || ne.getMicros() > lastNoteEvent.getMicros())
					lastNoteEvent = clone;
				if(ne == activeEvent)
					this.activeEvent = clone;
			}
			
			if(adjustHeight) //if we can adjust the height of the note, play it
				noteEventGuiModel.playNote((byte)noteEventGuiModel.getActiveEvent().getNote());	
		}

		public void mouseReleased(MouseEvent e) {
			//this mode finishes when the mouse is released
			uiModeHandler.removeMode(this);
			
			if(adjustHeight)
				noteEventGuiModel.playNote((byte)-1);	
		}

		public void mouseDragged(MouseEvent e) {
			MidiTransaction<NoteEvent> temp = noteEventGroup.createTransaction();
			
			long micros = graph.pixelsToMicros(e.getX());
			
			long originalLength = moveStartFlag ? (lastNoteEvent.getEndMicros() - activeEvent.getMicros())
					: activeEvent.getEndMicros() - firstNoteEvent.getMicros();
			
			if(originalLength < 1) originalLength = 1;
			
			long newLength = moveStartFlag ? (lastNoteEvent.getEndMicros() - micros)
					: micros - firstNoteEvent.getMicros();
			
			if(newLength < 1) newLength = 1; 
			
			double compressionRatio = (double)newLength / originalLength;
			
			Iterator<NoteEvent> curr = modifiedNotes.iterator();
			Iterator<NoteEvent> orig = originalNotes.iterator();
			
			while(curr.hasNext())
			{
				NoteEvent currNoteEvent = curr.next();
				NoteEvent origNoteEvent = orig.next();
				
				ChangeNoteOperation op = new ChangeNoteOperation(currNoteEvent);
					
				if(moveStartFlag)
				{
					op.setNewMicros((long) ((origNoteEvent.getMicros() - lastNoteEvent.getEndMicros()) * compressionRatio
							+ lastNoteEvent.getEndMicros()));
					op.setNewEndMicros((long) ((origNoteEvent.getEndMicros() - lastNoteEvent.getEndMicros()) * compressionRatio
							+ lastNoteEvent.getEndMicros()));
				}
				else
				{
					op.setNewMicros((long) ((origNoteEvent.getMicros() - firstNoteEvent.getMicros()) * compressionRatio
							+ firstNoteEvent.getMicros()));
					op.setNewEndMicros((long) ((origNoteEvent.getEndMicros() - firstNoteEvent.getMicros()) * compressionRatio
							+ firstNoteEvent.getMicros()));
				}
				
				if(adjustHeight)
				{
					op.setNewNote(graph.pixelsToNote(e.getY()));
				}
				
				temp.add(op);
				
				if(adjustHeight) //if adjust height is allowed, only one note should be selected
					noteEventGuiModel.playNote(currNoteEvent.getNote());	
			}

			temp.doIt();
		}
	}

	/**
	 * For moving the body of a note
	 */
	private class MoveNotesUIMode extends MouseInputUIMode
	{
		private int currX;
		private Collection<NoteEvent> notesToMove;

		public MoveNotesUIMode(int startX, Collection<NoteEvent> notesToMove)
		{
			this.currX = startX;
			this.notesToMove = notesToMove;
			
			//if moving only one note, the height can be adjusted
			if(notesToMove.size() == 1)
				noteEventGuiModel.playNote((byte)notesToMove.iterator().next().getNote());	

			//noteEventGuiModel.getUndoLog().startTransaction();
		}
		
		public void mouseReleased(MouseEvent e) {
			//this mode finishes when the mouse is released
			uiModeHandler.removeMode(this);
			noteEventGuiModel.playNote((byte)-1);	
		}

		public void mouseDragged(MouseEvent e) {
	
			MidiTransaction<NoteEvent> changeMicros = noteEventGroup.createTransaction();
			
			long microsDelta = graph.pixelsToRelMicros(e.getX()- currX);
			
			//if there is only one selected note, allow it to be moved up and down
			if(notesToMove.size() == 1)
			{
				//seperate out into two transactions so the note doesn't get stuck 
				//when moved to the side
				MidiTransaction<NoteEvent> changeNote = noteEventGroup.createTransaction();
				
				NoteEvent ne = notesToMove.iterator().next();
				
				noteEventGuiModel.playNote((byte)ne.getNote());	

				ChangeNoteOperation op = new ChangeNoteOperation(ne);
				op.setNewNote(graph.pixelsToNote(e.getY())); 
				changeNote.add(op);
				changeNote.doIt();
				
				op = new ChangeNoteOperation(ne);
				op.setMicrosDelta(microsDelta);
				op.setEndMicrosDelta(microsDelta);
				
				changeMicros.add(op);
			}
			else
			{
				if(microsDelta == 0) return;
				
				for(NoteEvent se : notesToMove)
				{
					ChangeNoteOperation op;
					changeMicros.add(op = new ChangeNoteOperation(se));
					op.setMicrosDelta(microsDelta);
					op.setEndMicrosDelta(microsDelta);
				}
			}
			
			if(changeMicros.doIt())
				currX = e.getX();

			//otherwise, merge it's changes into the current transaction
			//TODO: DO LATER (for undo)
			//noteEventGuiModel.getUndoLog().getCurrTransaction().merge(temp);			
		}
	}

	/**
	 * For when the sampled clip is playing
	 */
	private class PlayingUIMode extends MouseInputUIMode
	{
	}
	
	/**
	 * Normal mode when no buttons are held down
	 */
	private class NormalUIMode implements UIMode, MouseListener, KeyListener
	{
		public void start(Component comp) {
			comp.addMouseListener(this);
			comp.addKeyListener(this);
		}

		public void stop(Component comp) {
			comp.removeMouseListener(this);
			comp.removeKeyListener(this);		
		}

	    public void mouseClicked(MouseEvent e) { 
	    }

		public void mousePressed(MouseEvent e) {
			graph.requestFocus();
			
			MousePositionStatus mps = graph.getMousePositionStatus(e.getX(), e.getY());
			
			if(e.isShiftDown())
			{
				if(mps.isNearNote() && noteEventGuiModel.getActiveEvent() != null)
				{
					//select a bunch of notes
					noteEventGuiModel.addSelectedEvents(midiTuneModel.getNoteEventGroup().
							getEventsBetween(mps.getNoteEvent(), noteEventGuiModel.getActiveEvent()));
				}
			}
//			else if(e.isControlDown())
//			{
//				//select another note
//				if(mps.isNearNote() && noteEventGuiModel.getActiveEvent() != null)
//				{
//					noteEventGuiModel.addSelectedEvent(mps.getNoteEvent());
//					noteEventGuiModel.setActiveEvent(mps.getNoteEvent());
//				}
//			}
			else if(e.isControlDown())
			{
				if(mps.isNearNote())
				{
					if(mps.isNearNoteBody())
					{
						System.out.println("move notes to end");
						
						List<NoteEvent> noteEvents = noteEventGroup.getEventsBetween(mps.closestEvent, noteEventGroup.getEvents().getLast());
						
						uiModeHandler.addMode(new MoveNotesUIMode(e.getX(), noteEvents));
					}
				}
			}
			else if(e.getButton()==e.BUTTON1) { //normal click
				
				noteEventGuiModel.clearSelectedEvents();

				if(mps.isNearNote()) //normal click near a node
				{
					
					noteEventGuiModel.setActiveEvent(mps.closestEvent);
					noteEventGuiModel.addSelectedEvent(noteEventGuiModel.getActiveEvent());
				}					
				else
				{
					noteEventGuiModel.setPastePointMicros(graph.pixelsToMicros(mps.x));
				}
			}//normal click
			else //second button click
			{
				if(!mps.isNearNote())
				{
					System.out.println("create new note");
					
					if(graph.pixelsToMicros(e.getX()) < 0) 
						return;
					
					//create a new note, and let the user drag the ending position of the
					//note
					noteEventGuiModel.clearSelectedEvents();
					noteEventGuiModel.setActiveEvent(
							midiTuneModel.getNoteEventGroup().createNoteEvent(graph.pixelsToNote(e.getY()),
							graph.pixelsToMicros(e.getX())));
					noteEventGuiModel.addSelectedEvent(noteEventGuiModel.getActiveEvent());
					uiModeHandler.addMode(new MoveNoteEndUIMode(false, true, noteEventGuiModel.getActiveEvent()));
				}
			}
			
			//
			//finally handle click drag sort of stuff
			//
			if(e.getButton()==e.BUTTON1) { //left button down 
				if(!e.isControlDown())
				{
					if(mps.isNearNoteBody())
					{
						System.out.println("move note body");
						uiModeHandler.addMode(new MoveNotesUIMode(e.getX(), noteEventGuiModel.getSelectedEvents()));
					}
					else if(mps.isNearNoteStart())
					{
						System.out.println("move note start");
						uiModeHandler.addMode(new MoveNoteEndUIMode(true, false, mps.getNoteEvent()));
					}
					else if(mps.isNearNoteEnd())
					{
						System.out.println("move note end");
						uiModeHandler.addMode(new MoveNoteEndUIMode(false, false, mps.getNoteEvent()));
					}
				}
			}

		}

		public void mouseReleased(MouseEvent e) {
		}

		public void mouseEntered(MouseEvent e) {
		}

		public void mouseExited(MouseEvent e) {
		}

		public void mouseDragged(MouseEvent e) {
			//playNoteFromPosition(e);		
		}

		public void mouseMoved(MouseEvent e) {
		}

		public void keyTyped(KeyEvent e) {
		}

		public void keyPressed(KeyEvent e) {
			if(e.getKeyCode() == KeyEvent.VK_DELETE)
			{
				noteEventGuiModel.deleteSelectedEvents();
			}
			else if(e.getKeyCode() == KeyEvent.VK_C && e.isControlDown()) //copy
			{
				noteEventGuiModel.copySelectedEvents();
			}
			else if(e.getKeyCode() == KeyEvent.VK_V && e.isControlDown()) //paste
			{
				//if the location selected is still on the screen
				if(noteEventGuiModel.getPastePointMicros() >= graph.getStartMicros() &&
						noteEventGuiModel.getPastePointMicros() < graph.getEndMicros())
					noteEventGuiModel.pasteSelectedEvents();
			}
			else if(e.getKeyCode() == KeyEvent.VK_X && e.isControlDown()) //cut
			{
				noteEventGuiModel.cutSelectedEvents();
			}
		}

		public void keyReleased(KeyEvent e) {
			// TODO Auto-generated method stub
			
		}
	}

}
