package kr.midireader;

import javax.sound.midi.MidiEvent;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;

import kr.miditunemodel.EventGroup;
import kr.miditunemodel.KREvent;
import kr.miditunemodel.MetrodomeEvent;
import kr.miditunemodel.MidiTuneModel;
import kr.miditunemodel.NoteEvent;
import kr.miditunemodel.NoteEventGroup;
import kr.util.MidiUtils;
import kr.util.MidiUtils.TempoCache;

import kr.util.Util;

/**
 * Analyzes a midi file for likely lyrics tracks, tune tracks and also can
 * determine whether the file is a kr midi
 */
public class MidiAnalyzer 
{
	//In KR, the midi files are not played at the exactly correct time the midi file specifies,
	//so we adjust for that
	public static final double KR_MICRO_DRIFT = 0.984177987009504;
	private static final int MIN_NOTE_LENGTH_MICROS = 25000;		

	
	public MidiAnalyzer()
	{
	}
	
	public static int guessLyricsTrack(Track [] tracks)
	{
		int maxLyrics = 0;
		int maxTrackNumber = -1;
		
		for(int i =0; i < tracks.length; i++)
		{
			Track t = tracks[i];
			
			int totalLyrics = 0;
			
			for(int j = 0; j < t.size(); j++)
			{
				MidiEvent e = t.get(j);	

				if(!isLyric(e))
					continue;

				String message = getMidiText(e.getMessage().getMessage());
				
				//ignore text matching krevents
				if(KREvent.Type.getTypeFromText(message) != null)
					continue;
				
				totalLyrics++;
			}
			
			if(totalLyrics > maxLyrics)
				maxTrackNumber = i;
		}
		
		return maxTrackNumber;
	}
	
	public static int guessTuneTrack(Track [] tracks, int lyricsTrackIndex)
	{
		//without lyrics we can't make a very good guess
		if(lyricsTrackIndex == -1) return -1;
		
		Track lyrics = tracks[lyricsTrackIndex];
		
		double bestScore = 0;
		int bestTrackIndex = -1;
		
		//
		// Find a track with the most notes that match the same ticks as the lyrics
		// and the least that don't
		//
		for(int i =0; i < tracks.length; i++)
		{
			//if(i == lyricsTrackIndex) continue;
			
			Track t = tracks[i];
			
			int totalLyricsMatchingMelody = 0;
			
			int lyricsIndex = 0;
			int noteIndex = 0;
			
			while(lyricsIndex < lyrics.size() || noteIndex < t.size())
			{
				MidiEvent le = lyricsIndex < lyrics.size() ? lyrics.get(lyricsIndex) : null;
				MidiEvent ne = noteIndex < t.size() ? t.get(noteIndex) : null;
				
				if(ne != null && !isNoteEvent(ne, true))
				{
					noteIndex ++;
					continue;
				}
				if(le != null && !isLyric(le))
				{
					lyricsIndex++;
					continue;
				}
				
				if(le == null || ne != null && le.getTick() > ne.getTick())
					noteIndex ++;
				else if(ne == null || le != null && le.getTick() < ne.getTick())
					lyricsIndex ++;
				else
				{
					totalLyricsMatchingMelody ++;
					noteIndex ++;
				}
			} // while counting the number of notes that match
			
			//base the score on the percentage of how many notes match lyrics out of the total notes
			//and how many lyrics match notes out of the total lyrics 
			double score = (double)totalLyricsMatchingMelody / noteIndex * (double)totalLyricsMatchingMelody / lyricsIndex;
			
			if(score > bestScore)
			{
				bestScore = score;
				bestTrackIndex = i;
			}
		} //for each track to check for a matching tune
		
		return bestTrackIndex;
			
	}
	
	public static String getTrackDescription(Track t) 
	{
		StringBuffer desc = new StringBuffer();
		for(int i = 0; i < t.size();i ++)
		{
			byte [] data = t.get(i).getMessage().getMessage();
			
			if(data[0] == -1 && data[1] == 3)
			{
				if(desc.length() != 0) desc.append('\n');
				desc.append(getMidiText(data));
			}
			
		}
		
		return desc.toString();
	}

	public static boolean guessIsKRMidi(Track [] tracks)
	{
		if(tracks.length == 3)
		{
			if(	getTrackDescription(tracks[1]).trim().toLowerCase().equals("vocals") &&
				getTrackDescription(tracks[2]).trim().toLowerCase().equals("events"))
				return true;
		}
		
		return false;
	}
	
	public static String getMidiText(byte[] data) {
		for(int i = 2; i < data.length;i++)
		{
			if((data[i] & 0x80) == 0)
				return new String(data,i+1,data.length-i-1);
		}
		
		throw new IllegalArgumentException("Cannot understand event "+Util.toHex(data));
	}
	
	public static void readKREvents(EventGroup<KREvent> krEvents, Sequence sequence, Track krEventTrack, boolean isKRMidi)
	{
		MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache(sequence); 
		
		for(int i = 0; i < krEventTrack.size(); i++)
		{
			MidiEvent e = krEventTrack.get(i);		

			if(!isLyric(e))
				continue;

			String message = getMidiText(e.getMessage().getMessage());
			
			long micros = tick2microsecond(sequence, e.getTick(), tempoCache, isKRMidi);
			
			KREvent.Type type = KREvent.Type.getTypeFromText(message);
			
			if(type != null)
				krEvents.addToEnd(new KREvent(type, micros));
		}
		krEvents.notifyChanged(true);
	}
	
	public static void readTempoEvents(EventGroup<MetrodomeEvent> mEvents, Sequence sequence, Track tempoTrack, boolean isKRMidi)
	{
		MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache(sequence); 
		
		for(int i = 0; i < tempoTrack.size(); i++)
		{
			MidiEvent e = tempoTrack.get(i);
			
			long tempoTicks = getTempoMicrosPerQtrTicks(e, isKRMidi);

			if(tempoTicks == -1) //not a tempo event
				continue;

			long micros = tick2microsecond(sequence, e.getTick(), tempoCache, isKRMidi);
			
			MetrodomeEvent me = new MetrodomeEvent(micros, tempoTicks);
			mEvents.addToEnd(me);
		}
		
		mEvents.notifyChanged(true);

	}
	
	/**
	 * Reads a midi track of voice tone for the song. This
	 * track may be the same as the lyrics.
	 * 
	 * @param lyrics lyrics track. May be null. May be the same as tune.
	 * @param tune the track which contains the tune
	 * @param channel 0x00 through 0x0F, indicates channel(s) to use for voice
	 */
	public static void readTune(NoteEventGroup noteEvents, Sequence sequence, Track lyrics, Track tune,
			boolean isKRMidi)
	{		
		
		NoteEvent currentNote = null;
		
		MidiUtils.TempoCache tempoCache = new MidiUtils.TempoCache(sequence); 
		
		for(int i = 0; i < tune.size(); i++)
		{
			MidiEvent e = tune.get(i);
			
			byte [] data = e.getMessage().getMessage();
			
			boolean is90 = (data[0] & 0xF0) == 0x90;
			boolean is80 = (data[0] & 0xF0) == 0x80;
			
			if(!is90 && !is80) //if not a note
				continue;

			boolean isOff = is90 &&  data[2] == 0 || is80;
			
			//if the note is turned off
			if(isOff)
			{
				//if there is a current note to turn off and it  
				if(currentNote != null && 
						currentNote.getNote() == data[1])
				{
					currentNote.setEndMicros(tick2microsecond(sequence, e.getTick(), tempoCache, isKRMidi));

					if(isGoodNote(currentNote))
						noteEvents.addToEnd(currentNote);
					
					currentNote = null;
				}
				continue;
			}
			else if(currentNote != null) //if another note is being played at the same time, turn off the first one
			{
				currentNote.setEndMicros(tick2microsecond(sequence, e.getTick(), tempoCache, isKRMidi));

				if(isGoodNote(currentNote))
					noteEvents.addToEnd(currentNote);
			}
			
			//note that the above code will alter a KR track for vocals somewhat, since the KR track sometimes plays two
			//notes for brief periods of time, and this code will chop one of the notes.  
			
			//on event
			currentNote = new NoteEvent(e.getMessage().getMessage(),  
					tick2microsecond(sequence, e.getTick(), tempoCache, isKRMidi));
			
		}

		if(lyrics == null) return;
		
		long minNoteMicros = 0;
		
		MidiUtils.TempoCache lyricsTempoCache = new MidiUtils.TempoCache(sequence); 

		for(int i = 0; i < lyrics.size(); i++)
		{
			MidiEvent l = lyrics.get(i);
			
			if(!isLyric(l))
				continue;

			String message = getMidiText(l.getMessage().getMessage());
			
			long micros = tick2microsecond(sequence, l.getTick(), lyricsTempoCache, isKRMidi);
			
			while(true)
			{
				NoteEvent n = noteEvents.getAtOrBeforeMicros(null,minNoteMicros, micros);
			
				if(n == null)
				{
					System.err.println("Not enough notes to process all the text, clipping "+message);
					break;
				}
				
				//we will have already dealt with this note event, so at a minimum, pick the next one
				minNoteMicros = n.getMicros()+1;
				
				if(n.getEndMicros() < l.getTick())
					continue;
				
				n.setKRText(message.trim());
				
				if((message.indexOf('\\')  != -1 || message.indexOf('/') != -1) && !n.isHead()) //sometimes used to simulate a new verse
				{
					NoteEvent p = (NoteEvent)n.getPrevious();
					p.setKRText(p.getText()+'.');
				}
				
				break;
			}
		} //for each lyric

		
		
		noteEvents.notifyChanged(true);
	}
	
	private static boolean isGoodNote(NoteEvent currentNote) {
		return currentNote.getNote() >= MidiTuneModel.MIN_NOTE && currentNote.getNote() <= MidiTuneModel.MAX_NOTE &&
		currentNote.getLength() > MIN_NOTE_LENGTH_MICROS;
	}

	/**
	 * @param onOnly true if only "on" notes events should be looked for
	 */
	private static boolean isNoteEvent(MidiEvent e, boolean onOnly) {
		byte [] data = e.getMessage().getMessage();
		
		boolean is90 = (data[0] & 0xF0) == 0x90;
		boolean is80 = (data[0] & 0xF0) == 0x80;
		
		if(onOnly)
			return is90 && data[2] != 0; //a 0x90 with a 0 note is considered off
		
		return is90||is80;
	}

	private static boolean isLyric(MidiEvent e) {
		byte [] data = e.getMessage().getMessage();
		if(data[0] != -1 || 
				data[1] != 1 && data[1] != 5)
			return false;
		return true;
	}

	private static long getTempoMicrosPerQtrTicks(MidiEvent e, boolean krDrift) {
		byte [] data = e.getMessage().getMessage();
		if(data[0] != -1 || 
				data[1] != 81 || data[2] != 3)
			return -1;
		
		long micros = (((long)((data[3] + 256) % 256)) << 16) | (((long)((data[4] + 256) % 256)) << 8) | ((long)((data[5] + 256) % 256));
		
		return krDrift ? Math.round(micros / KR_MICRO_DRIFT) : micros;
	}

	private static long tick2microsecond(Sequence sequence, long tick, TempoCache tempoCache, boolean isKRMidi) {
		long micros = MidiUtils.tick2microsecond(sequence, tick, tempoCache);
		if(isKRMidi)
			return Math.round(micros / KR_MICRO_DRIFT);
		return micros;
	}

	public static void createDefaultKREvents(MidiTuneModel midiTuneModel, EventGroup<KREvent> krEvents) {
		krEvents.clear();
		krEvents.addToEnd(new KREvent(KREvent.Type.BASS_ON, 2 * 1000000));
		krEvents.addToEnd(new KREvent(KREvent.Type.GTR_ON, 2 * 1000000));
		krEvents.addToEnd(new KREvent(KREvent.Type.KEYS_ON, 2 * 1000000));
		krEvents.addToEnd(new KREvent(KREvent.Type.DRUM_ON, 2 * 1000000));
		
		NoteEvent ne = midiTuneModel.getNoteEventGroup().getEvents().getFirst();
		
		if(ne == null) // no note event, not much to do here 
		{
			int endPos = Math.max(2 * 1000000, midiTuneModel.getMaxMilliSeconds() * 1000 - 2 * 1000000);
			
			krEvents.addToEnd(new KREvent(KREvent.Type.SINGER_FINAL_POS, endPos));
			
			krEvents.addToEnd(new KREvent(KREvent.Type.WIN_LOSE, endPos));
			krEvents.addToEnd(new KREvent(KREvent.Type.GTR_OFF, endPos));
			krEvents.addToEnd(new KREvent(KREvent.Type.KEYS_OFF, endPos));
			krEvents.addToEnd(new KREvent(KREvent.Type.DRUM_OFF, endPos));
			krEvents.addToEnd(new KREvent(KREvent.Type.SONG_END, endPos));
		}
		else {
			long lastNoteEndMicros = ne.getEndMicros();
			
			krEvents.addToEnd(new KREvent(KREvent.Type.INTRO_END, Math.max(ne.getMicros() - 2 * 1000000, 0)));

			for(;!ne.isTail(); ne = (NoteEvent)ne.getNext())
			{
				if(ne.getMicros() - lastNoteEndMicros > 10 * 1000000)
				{
					krEvents.addToEnd(new KREvent(KREvent.Type.SINGER_MIC_DOWN, lastNoteEndMicros + 2 * 1000000));
					krEvents.addToEnd(new KREvent(KREvent.Type.SINGER_MIC_UP, ne.getMicros() - 2 * 1000000));
				}
				lastNoteEndMicros = ne.getEndMicros();
			}
	
			krEvents.addToEnd(new KREvent(KREvent.Type.SINGER_FINAL_POS, ne.getMicros()));
			
			krEvents.addToEnd(new KREvent(KREvent.Type.WIN_LOSE, ne.getEndMicros() + 2 * 1000000));
			krEvents.addToEnd(new KREvent(KREvent.Type.GTR_OFF, ne.getEndMicros() + 2 * 1000000));
			krEvents.addToEnd(new KREvent(KREvent.Type.KEYS_OFF, ne.getEndMicros() + 2 * 1000000));
			krEvents.addToEnd(new KREvent(KREvent.Type.DRUM_OFF, ne.getEndMicros() + 2 * 1000000));
			krEvents.addToEnd(new KREvent(KREvent.Type.SONG_END, ne.getEndMicros() + 5 * 1000000));
		}
		
		krEvents.notifyChanged(true);
	}

	
}
