package kr.midireader.krwriter;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Track;

import kr.midireader.MidiAnalyzer;
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.NotificationManager;
import kr.util.MidiUtils.TempoCache;

public class KRWriter {

	private static int KR_RESOLUTION = 480;

	public Sequence createSequence(NoteEventGroup neg, EventGroup<KREvent> keg,
			EventGroup<MetrodomeEvent> meg) {
		try {
			Sequence seq = new Sequence(Sequence.PPQ, KR_RESOLUTION);

			TempoCache tc = new TempoCache();

			Track timingTrack = seq.createTrack();

			timingTrack.add(new MidiEvent(createTrackLabel("Master Track"), 0));
			timingTrack.add(new MidiEvent(createTimeSignature((byte) 4,
					(byte) 2, (byte) 0x18, (byte) 8), 0)); // common
			// time
			// signature

			MetrodomeEvent me = meg.getEvents().getFirst();

			if (me != null) {
				if (me.getMicros() != 0)
					timingTrack.add(new MidiEvent(createTempo(500000), 0));
				else
					timingTrack.add(new MidiEvent(createTempo(me
							.getQuarterNoteMicros()), 0));

				while ((me = (MetrodomeEvent) me.getNext()) != null) {
					// PERF: slow, but who cares?
					tc.refresh(seq);
					long ticks = calcTicks(me.getMicros(), seq, tc);
					timingTrack.add(new MidiEvent(createTempo(me
							.getQuarterNoteMicros()), ticks));
				}
			}

			tc.refresh(seq);

			Track tuneTrack = seq.createTrack();

			tuneTrack.add(new MidiEvent(createTrackLabel("VOCALS"), 0));

			NoteEvent ne = neg.getEvents().getFirst();

			int hcnt = 0;

			while (ne != null) {
				long ticks = calcTicks(ne.getMicros(), seq, tc);
				long endTicks = calcTicks(ne.getEndMicros(), seq, tc);

				// HACK
				/*
				 * ticks = 10000 + hcnt * 203952 / 318; endTicks = ticks + 200;
				 * hcnt++;
				 */
				// END_HACK
				String text = ne.getText();

				tuneTrack
						.add(new MidiEvent(createLyric(text, (byte) 5), ticks));
				tuneTrack.add(new MidiEvent(createTune(ne.getNote(), true),
						ticks));
				tuneTrack.add(new MidiEvent(createTune(ne.getNote(), false),
						endTicks));
				ne = (NoteEvent) ne.getNext();
			}

			Track eventTrack = seq.createTrack();

			eventTrack.add(new MidiEvent(createTrackLabel("EVENTS"), 0));

			KREvent ke = keg.getEvents().getFirst();

			while (ke != null) {
				long ticks = calcTicks(ke.getMicros(), seq, tc);

				eventTrack.add(new MidiEvent(createLyric(ke.getKRType()
						.getText(), (byte) 1), ticks));
				ke = (KREvent) ke.getNext();
			}

			return seq;

		} catch (InvalidMidiDataException e) {
			e.printStackTrace();
			NotificationManager.inst().error("Internal error "+e);
		}

		return null;
	}

	public void write(NoteEventGroup neg, EventGroup<KREvent> keg,
			EventGroup<MetrodomeEvent> meg, String file) throws IOException {
		Sequence seq = createSequence(neg, keg, meg);

		OutputStream stream = new BufferedOutputStream(new FileOutputStream(
				file));

		MidiSystem.write(seq, 1, stream);
		stream.close();
	}

	private long calcTicks(long micros, Sequence seq, TempoCache tc) {
		return MidiUtils.microsecond2tick(seq, Math.round((double) micros
				* MidiAnalyzer.KR_MICRO_DRIFT), tc);
	}

	private ShortMessage createTune(byte note, boolean noteOn)
			throws InvalidMidiDataException {
		ShortMessage sm = new ShortMessage();
		sm.setMessage(0x90, 0, note, noteOn ? 0x4c : 0);

		return sm;
	}

	private MetaMessage createLyric(String text, byte type)
			throws InvalidMidiDataException {
		MetaMessage m = new MetaMessage();

		m.setMessage(type, text.getBytes(), text.length());

		return m;
	}

	private long calcTicks(long micros, int tempoMPQ, int resolution) {
		return Math.round((double) micros * MidiAnalyzer.KR_MICRO_DRIFT
				* resolution / tempoMPQ);
	}

	private MetaMessage createTempo(long micros)
			throws InvalidMidiDataException {

		micros = Math.round((double) micros * MidiAnalyzer.KR_MICRO_DRIFT);

		MetaMessage m = new MetaMessage();

		byte[] b = { (byte) (micros >> 16), (byte) ((micros >> 8) % 256),
				(byte) (micros % 256) };
		m.setMessage(0x51, b, b.length);
		return m;
	}

	private MetaMessage createTimeSignature(byte num, byte denom,
			byte metronome, byte notatedNotes) throws InvalidMidiDataException {
		MetaMessage m = new MetaMessage();

		m.setMessage(0x58, new byte[] { num, denom, metronome, notatedNotes },
				4);
		return m;
	}

	private MetaMessage createTrackLabel(String text)
			throws InvalidMidiDataException {
		MetaMessage m = new MetaMessage();

		m.setMessage(0x03, text.getBytes(), text.length());

		return m;
	}

	public Sequence createSequence(MidiTuneModel midiModel) {
		return createSequence(midiModel.getNoteEventGroup(), midiModel.getKREventGroup(), midiModel.getMetrodomeEventGroup());
	}

}
