package kr.gui.midigraph;

import java.awt.Color;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.swing.BoxLayout;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.event.MouseInputListener;

import kr.util.Util;

import kr.AudioChannelModel;
import kr.AudioModel;
import kr.gui.GuiModel;
import kr.gui.XScrollingPane;
import kr.gui.GuiModel.MessageType;
import kr.miditunemodel.Event;
import kr.miditunemodel.MetrodomeEvent;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEvent;
import kr.util.Listener;
import kr.util.ListenerManager;
import kr.util.audio.AudioClip;
import kr.util.audio.MidiAudioClip;
import kr.util.audio.MonoAudioClip;
import kr.util.ft.SubtractScaleData;
import kr.util.ft.FFTScaleData;
import kr.util.ft.SpectrumAnalyzer;

public class MidiTuneGraph extends MidiGraph implements Listener {
	
	private static final Color TICK_START_COLOR = Color.BLUE;
	private static final Color TICK_COLOR = Color.getHSBColor((float)(160/255.), (float)(80/255.), 1);
	private static final double QUARTER_NOTES_PER_MARK = .5;
	private static final Color TICK_SELECTED_COLOR = Color.BLACK;


	private MidiTuneModel model;
	
	private NoteUI noteUI;
	private ColoredScaleUI scaleUI;
	
	private AudioModel audioModel;

	public MidiTuneGraph(GuiModel guiModel, MidiTuneModel midiModel, MidiTuneGraphController midiController,
			AudioModel audioModel) { 
		super(guiModel);
		setFocusable(true);
		setOpaque(true);
		this.model = midiModel;
		this.audioModel = audioModel;
		midiController.init(guiModel, guiModel.getNoteEventGuiModel(), midiModel, this, audioModel);
		
		//TODO: consider using a container (for this and a lot of things)
		scaleUI = new ColoredScaleUI(guiModel, this, MidiTuneModel.MIN_NOTE, MidiTuneModel.MAX_NOTE);//new ScaleUI(guiModel, this, MidiTuneModel.MIN_NOTE, MidiTuneModel.MAX_NOTE);
		noteUI = new NoteUI(scaleUI, this);
		
		//freq in hz
		double minFreq = MidiAudioClip.getFreqForMidi(MidiTuneModel.MIN_NOTE);
		double maxFreq = MidiAudioClip.getFreqForMidi(MidiTuneModel.MAX_NOTE);
		
		
		//freq in cycles per SAMPLE SIZE frames
		int start = (int)Math.floor(minFreq * SpectrumAnalyzer.SAMPLE_SIZE / 48000.);
		int end = (int)Math.ceil(maxFreq * SpectrumAnalyzer.SAMPLE_SIZE / 48000.);
		
		double [] freqs = new double[end - start + 1]; 

		for(int i = start; i <= end; i++)
		{
			freqs[i - start] = ((double)i) / SpectrumAnalyzer.SAMPLE_SIZE * 48000.;
		}

	}

	private static final long serialVersionUID = 1L;


	public void notify(Object source, Object type, Object... values) {
		if(source == guiModel)
		{
			if(type ==  GuiModel.MessageType.NOW_UPDATED)
			{
				long oldMicros = (Long)values[0];
				//TODO: PERF: make this faster
				this.repaint();
			}
		}
	}


	@Override
	protected void doScrollingPaint(Graphics2D g, int startX, int endX) {
	    int w = getWidth();
	    int h = getHeight();
	    //System.out.println("clipping rect is "+g.getClipBounds());

        g.setColor(Color.WHITE);
        g.fillRect(startX, 0, endX-startX, h);

        int nowX = guiModel.calcNowX(w);
        
        long nowMicros = guiModel.getCurrMicros();
        
        //g.setClip(startX, 0, endX - startX, h);
        
        //
        // Draw scale
        //
        //scaleUI.drawScale(g, startX, endX, spectrumAnalyzer, spectrumAnalyzer2);
        scaleUI.drawScale(g, startX, endX);

        //
        // Draw notes
        //
        long startMicros = guiModel.pixelsToMicros(startX, w);
        long endMicros = guiModel.pixelsToMicros(endX, w);

        //get all notes that are within the screen
        //PERF specify a starting point
        NoteEvent n = model.getNoteEventGroup().getAtOrBeforeMicros(null, 0, startMicros);
                
        if(n == null) n = model.getNoteEventGroup().getEvents().getFirst();
        
        while(n != null && n.getMicros() < endMicros)
        {
        	if(n.isContinuation())
        	{
        		noteUI.drawContinuationLine(g, (NoteEvent) n.getPrevious(),	n);
        	}

        	noteUI.drawNoteLine(g, n, guiModel.getNoteEventGuiModel().isSelectedEvent(n));
        	n = (NoteEvent)n.getNext();
        }

        //if we are off the edge of the paintable area, but are a continuation, the
        //continuation line will go into the paintable area
    	if(n != null && n.isContinuation())
    	{
    		noteUI.drawContinuationLine(g, (NoteEvent) n.getPrevious(),	n);
    	}


    	//
    	// Draw metrodome ticks
    	//
        //get all notes that are within the screen
        //PERF specify a starting point
        MetrodomeEvent m = model.getMetrodomeEventGroup().getAtOrBeforeMicros(null, 0, startMicros);
        if(m == null) m = model.getMetrodomeEventGroup().getEvents().getFirst(); 
        
        while(m != null && m.getMicros() < endMicros)
        {
			drawDivisions(g, startX, endX, m.getMicros(), m.getEndMicros(), m.getQuarterNoteMicros(), guiModel.getMetrodomeGuiModel().isSelectedEvent(m));
        	m = (MetrodomeEvent)m.getNext();
		}
    	
    	
    	//g.setClip(0, 0, w, h);
	}
	
	private void drawDivisions(Graphics2D g, int startX, int endX, long micros, long endMicros, long quarterNoteMicros, boolean selected) {
		int x1 = microsToPixels(micros);
		startX = Math.max(x1, startX);
		
		int x2 = endMicros == -1 ?  getWidth(): Math.min(getWidth(), microsToPixels(endMicros));
		endX = Math.min(x2, endX);
		
		if(endX <= startX) return;

		int h = getHeight();
		double tickPixels = guiModel.getPixelsToMicrosRatio() * quarterNoteMicros;
		
		g.setColor(TICK_COLOR);

		if(startX <= x1)
		{
			g.drawLine(x1, 0, x1, h);
		}
		
		int c = (int)Math.max(1,Math.floor((startX - x1) / tickPixels / QUARTER_NOTES_PER_MARK));
		while(true)
		{
			int x = (int) Math.round(tickPixels * QUARTER_NOTES_PER_MARK *  c++ + x1);
			if(x > endX)
				break;
			g.drawLine(x, 0, x, h);
		}
	}

	public byte pixelsToNote(int y) {
		return scaleUI.pixelsToNote(y);
	}



	public MousePositionStatus getMousePositionStatus(int x, int y) {
		long micros = pixelsToMicros(x);
		
		//PERF: specify a starting point
		NoteEvent closestNote = model.getNoteEventGroup().getClosestEvent(null, micros);
		
		return new MousePositionStatus(x, y, closestNote);
	}


	@Override
	public Event getEvent(int x, int y) {
		MousePositionStatus s = getMousePositionStatus(x,y);
		if(s.isNearNote())
			return s.closestEvent;
		return null;
	}


	public void updateScale() {
		scaleUI.setScaleData(guiModel.getFFTScaleData());
		repaintScrollingData();
	}

	public void clearScale() {
		scaleUI.setScaleData(null);
		repaintScrollingData();
	}

	public class MousePositionStatus {
		int x, y;
		NoteEvent closestEvent; 
		int noteStartX, noteEndX, noteY;
		
		/**
		 * Threshold for a position being close enough to a note end
		 * to select it, in both x and y
		 */
		private static final int NOTE_END_THRES = 4;
		
		/**
		 * Threshold for a position being close enough to a note body
		 * to select it, for the y direction only
		 */
		private static final int NOTE_Y_THRES = 5;
		
		public MousePositionStatus(int x, int y, NoteEvent closestNote) {
			this.x = x;
			this.y = y;
			this.closestEvent = closestNote;
			if(closestNote != null)
			{
				this.noteStartX = microsToPixels(closestNote.getMicros()); 
				this.noteEndX = microsToPixels(closestNote.getEndMicros()); 
				this.noteY = scaleUI.noteToPixels(closestNote.getNote());
			}
			
		}

		/**
		 * Determines if the position is close enough to the note to be considered
		 * selecting it, or directly below or above it.
		 */
		public boolean isNearNote() {
			return closestEvent != null && x > noteStartX - NOTE_END_THRES && x < noteEndX + NOTE_END_THRES;
		}

		public boolean isNearNoteStart() {
			return closestEvent != null && Math.abs(y - noteY) <= NOTE_END_THRES
			&& Math.abs(x - noteStartX) <= NOTE_END_THRES;
		}

		public boolean isNearNoteEnd() {
			return closestEvent != null && Math.abs(y - noteY) <= NOTE_END_THRES
			&& Math.abs(x - noteEndX) <= NOTE_END_THRES;
		}

		public boolean isNearNoteBody() {
			return closestEvent != null &&  x > noteStartX + NOTE_END_THRES && x < noteEndX - NOTE_END_THRES
			&& Math.abs(y - noteY) <= NOTE_Y_THRES;
		}

		public NoteEvent getNoteEvent() {
			return closestEvent;
		}
	}


}

class MidiGraphTest
{
	public static void main(String [] args) throws Exception
	{
		Sequence sequence = null;
		File file = new File(args[0]);

		sequence = MidiSystem.getSequence(file);

		Track [] tracks = sequence.getTracks();
		
		MidiTuneModel m = new MidiTuneModel();
		
		m.readTune(sequence, tracks[1], tracks[1], false);


		JFrame f = new JFrame();

		Container content = f.getContentPane();
		content.setBackground(Color.white);
		content.setLayout(new BoxLayout(content,
                BoxLayout.PAGE_AXIS));
		
		AudioModel am;
		GuiModel g = new GuiModel(am=new AudioModel(),m); 
		MidiTuneGraphController mtc = new MidiTuneGraphController();
		MidiTuneGraph mg;
		
		content.add(mg=new MidiTuneGraph(g, m, mtc, am));
		
		f.pack();
		f.setSize(600, 300);
		f.setVisible(true);
	}
}
