package kr.miditunemodel;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;

import kr.miditunemodel.Event.Type;
import kr.miditunemodel.MidiTuneModel.MessageType;
import kr.undo.Transaction;
import kr.util.ListenerManager;

import kr.util.LinkedList;
import kr.util.Util;
import kr.util.LinkedList.ConstantComparator;

public class EventGroup<T extends Event> 
{
	public static enum MessageType { EVENTS_CHANGED, LENGTH_CHANGED }; 
	
	//this is a list rather then a sorted set because a sorted set cannot
	//return an artibrary index or iterate backwards through a sub set,
	//which is needed to display the graph of midi notes
	protected LinkedList<T> events = new LinkedList<T>();

	private static class ConstantComparator<T2 extends Event> implements LinkedList.ConstantComparator<T2>
	{
		private enum Type { MICROS, CLOSEST_EVENT }

		private Type type;
		private long value;
		
		public ConstantComparator(Type t, long value)
		{
			this.type = t;
			this.value = value;
		}

		public int compareToConstant(T2 t) {
			if(type == Type.MICROS)
			{
				long v = t.getMicros() - value;
				if(v < 0)
					return -1;
				if(v == 0)
					return 0;
				return 1;
			}
			if(type == Type.CLOSEST_EVENT)
			{
				long v = t.getMicros() - value;
				long v2 = t.getEndMicros() - value;
				if(v2 < v) v = v2;
				
				if(v < 0)
					return -1;
				if(v == 0)
					return 0;
				return 1;
			}
				
			throw new IllegalStateException("shouldn't be here");
		}
	}
	
	/**
	 * Finds the event that has either a start or end endpoint that is closest to the given micros
	 */
	public T getClosestEvent(T startingPoint, long micros) 
	{
		T event = getNextEvent(startingPoint, micros);
		
		if(event == null) event = events.getLast();
		
		if(event == null) return null;
		
		return (T) (event.isHead() || event.getMicros() - micros
			< micros - ((Event) event.getPrevious()).getEndMicros() ? event : event.getPrevious());
	}

	/**
	 * Returns the next event the starts at or beyond the micros value given.
	 */
	public T getNextEvent(T startingPoint, long micros) {
		ConstantComparator<T> comparator = new ConstantComparator<T>(ConstantComparator.Type.MICROS, micros);
		T value = events.getElementMatchingComparator(startingPoint, comparator, false);
		
		return value;
	}

	/**
	 * Returns the event that starts at or before the micros value given.
	 */
	public T getPrevEvent(T startingPoint, long micros) {
		ConstantComparator<T> comparator = new ConstantComparator<T>(ConstantComparator.Type.MICROS, micros);
		T value = events.getElementMatchingComparator(startingPoint, comparator, true);
		
		return value;
	}

	/**
	 * Returns the event with the greatest value that is at or before the given micros.
	 * However, if the event is before "minNoteMicros" then null will be returned 
	 */
	public T getAtOrBeforeMicros(T startingPoint, long minNoteMicros, long micros) 
	{
		ConstantComparator<T> comparator = new ConstantComparator<T>(ConstantComparator.Type.MICROS, micros);
		T value = events.getElementMatchingComparator(startingPoint, comparator, true);
		if(value == null) return null;
		
		if(value.getMicros() < minNoteMicros) return null;
		
		return value;
	}
	

	public LinkedList<T> getEvents() {
		return events;
	} 
	

	public int getMaxMilliSeconds() {
		if(getEvents().size() == 0) return 0;
		return (int)(getEvents().getLast().getEndMicros()/1000+1);
	}

	public int getNumEvents() {
		return events.size();
	}

	public void clear() {
		events.clear();
	}
	
//	public void updateEndMicrosForEvent(Event e, long micros) {
//		long lastEndMicros = e.getEndMicros();
//		e.setEndMicros(micros);
//		
//		if(reseatEvent(e))
//		{
//			ListenerManager.inst().notify(this, MessageType.NOTE_UPDATED);
//			if(e.isTail())
//				ListenerManager.inst().notify(this, MessageType.LENGTH_UPDATED);
//		}
//		else
//			e.setEndMicros(lastEndMicros);
//	}
//
//	public void updateMicrosForEvent(Event e, long micros) 
//	{
//		long lastMicros = e.getMicros();
//		e.setMicros(micros);
//		if(reseatEvent(e))
//		{
//			ListenerManager.inst().notify(this, MessageType.NOTE_UPDATED);
//			if(e.isTail())
//				ListenerManager.inst().notify(this, MessageType.LENGTH_UPDATED);
//		}
//		else
//			e.setMicros(lastMicros);
//	}
//
//	public void moveEvent(Event e, long microsDelta) 
//	{
//		e.setMicros(e.getMicros()+microsDelta);
//		e.setEndMicros(e.getEndMicros()+microsDelta);
//		if(reseatEvent(e))
//		{
//			ListenerManager.inst().notify(this, MessageType.NOTE_UPDATED);		
//			if(e.isTail())
//				ListenerManager.inst().notify(this, MessageType.LENGTH_UPDATED);
//		}
//		else
//			e.setMicros(lastMicros);
//	}
//
//	public void moveEvents(Collection<Integer> selectedNotes, long microsDelta) {
//		for(Integer index : selectedNotes)
//		{
//			T e = events.get(index);
//			e.setMicros(e.getMicros()+microsDelta);
//			e.setEndMicros(e.getEndMicros()+microsDelta);
//			if(index == events.size() -1)
//				ListenerManager.inst().notify(this, MessageType.LENGTH_UPDATED);
//		}
//
//		ListenerManager.inst().notify(this, MessageType.NOTE_UPDATED);
//	}

	public void addToEnd(T event) {
		events.addToEnd(event);

	}

	/**
	 * Reorders events in the model. Also checks whether the model is in a valid state. 
	 * @param the events which have changed since the last time the model has been
	 * in a valid state.
	 */
	public boolean reseat(Collection<T> changedEvents) 
	{
		//first, arrange the events in the correct order
		for(T e : changedEvents)
		{
			while(e.getPrevious() != null && ((T) e.getPrevious()).getMicros() > e.getMicros())
				events.swapWithPrevious(e);
			while(e.getNext() != null && ((T) e.getNext()).getMicros() < e.getMicros())
				events.swapWithNext(e);
		}

//		List<T> a = new ArrayList();
//		List<T> b = new ArrayList();
//		//hack test
//		for(T e = events.getFirst(); e != null; e = (T)e.getNext())
//		{
//			a.add(e);
//		}
//		
//		for(T e = events.getLast(); e != null; e = (T)e.getPrevious())
//		{
//			b.add(0,e);
//		}
//		
//		if(!a.equals(b))
//		{
//			System.out.println("they are not equal");
//		}

		//then make sure that they don't overlap
		for(T e : changedEvents)
		{
			if(e.getPrevious() != null && ((Event) e.getPrevious()).getEndMicros() > e.getMicros() || 
					e.getNext() != null && ((Event) e.getNext()).getMicros() < e.getEndMicros())
				return false;
		}
		
		
		return true;
	}
	
	public void insertEvent(T newItem) {
		T insertPoint = getNextEvent(null, newItem.getMicros());
		if(insertPoint == null)
			events.addToEnd(newItem);
		else
			events.insertAt(insertPoint, newItem);
	}


	public void removeEvent(T e) {
		events.remove(e);
	}

	/**
	 * Gets events inbetween a and b inclusive
	 */
	public List<T> getEventsBetween(T a, T b) 
	{
		T t;
		
		if(a.getMicros() > b.getMicros())
		{
			t = b;
			b = a;
			a = t;
		}
		
		List<T> inbetween = new ArrayList<T>();
		
		t = a;
		while(t != b)
		{
			inbetween.add(t);
			t=(T) t.getNext();
		}
		
		inbetween.add(b);
		
		return inbetween;
	}

	/**
	 * Notifies listeners when events change
	 */
	public void notifyChanged(boolean containsLastEvent) {
		ListenerManager.inst().notify(this, MessageType.EVENTS_CHANGED);
		
		if(containsLastEvent)
			ListenerManager.inst().notify(this, MessageType.LENGTH_CHANGED);
	}

	/**
	 * Creates a transaction specific to this event group, may be overridden
	 */
	public MidiTransaction createTransaction() {
		return new MidiTransaction(this);
	}

}
