package org.herac.tuxguitar.io.midi;

import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.herac.tuxguitar.io.midi.base.MidiEvent;
import org.herac.tuxguitar.io.midi.base.MidiMessage;
import org.herac.tuxguitar.io.midi.base.MidiSequence;
import org.herac.tuxguitar.io.midi.base.MidiTrack;

public class MidiFileReader implements MidiFileHeader{
	
	public static boolean CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX = true;
	
	private static final int STATUS_NONE = 0;
	private static final int STATUS_ONE_BYTE = 1;
	private static final int STATUS_TWO_BYTES = 2;
	private static final int STATUS_SYSEX = 3;
	private static final int STATUS_META = 4;
	
	public MidiSequence getSequence(InputStream stream)throws MidiFileException, IOException{
		DataInputStream	in = new DataInputStream(stream);
		if (in.readInt() != HEADER_MAGIC){
			throw new MidiFileException("not a MIDI file: wrong header magic");
		}
		int headerLength = in.readInt();
		if (headerLength < HEADER_LENGTH){
			throw new MidiFileException("corrupt MIDI file: wrong header length");
		}
		int type = in.readShort();
		if (type < 0 || type > 2){
			throw new MidiFileException("corrupt MIDI file: illegal type");
		}
		if (type == 2){
			throw new MidiFileException("this implementation doesn't support type 2 MIDI files");
		}
		int trackCount = in.readShort();
		if (trackCount <= 0){
			throw new MidiFileException("corrupt MIDI file: number of tracks must be positive");
		}
		if (type == 0 && trackCount != 1){
			throw new MidiFileException("corrupt MIDI file: type 0 files must contain exactly one track");
		}
		float divisionType = -1.0F;
		int resolution = -1;
		int division = in.readUnsignedShort();
		if ((division & 0x8000) != 0){
			int frameType = -((division >>> 8) & 0xFF);
			if(frameType == 24){
				divisionType = MidiSequence.SMPTE_24;
			}else if(frameType == 25){
				divisionType = MidiSequence.SMPTE_25;
			}else if(frameType == 29){
				divisionType = MidiSequence.SMPTE_30DROP;
			}else if(frameType == 30){
				divisionType = MidiSequence.SMPTE_30;
			}else{
				throw new MidiFileException("corrupt MIDI file: illegal frame division type");
			}
			resolution = division & 0xff;
		}else{
			divisionType = MidiSequence.PPQ;
			resolution = division & 0x7fff;
		}
		
		in.skip(headerLength - HEADER_LENGTH);
		
		MidiSequence sequence = new MidiSequence(divisionType,resolution);
		for (int i = 0; i < trackCount; i++){
			MidiTrack track = new MidiTrack();
			sequence.addTrack(track);
			readTrack(in, track);
		}
		
		in.close();
		
		return sequence;
	}
	
	private void readTrack(DataInputStream in, MidiTrack track)throws MidiFileException, IOException{
		while (true){
			if (in.readInt() == TRACK_MAGIC){
				break;
			}
			int chunkLength = in.readInt();
			if (chunkLength % 2 != 0){
				chunkLength++;
			}
			in.skip(chunkLength);
		}
		
		MidiTrackReaderHelper helper = new MidiTrackReaderHelper(0,in.readInt(),-1);
		while (helper.remainingBytes > 0){
			helper.ticks += readVariableLengthQuantity(in, helper);
			MidiEvent event = readEvent(in, helper);
			if(event != null){
				track.add(event);
			}
		}
	}
	
	private static MidiEvent readEvent(DataInputStream in, MidiTrackReaderHelper helper)throws MidiFileException, IOException{
		int statusByte = readUnsignedByte(in, helper);
		int savedByte = 0;
		boolean runningStatusApplies = false;
		
		if (statusByte < 0x80){
			if (helper.runningStatusByte != -1){
				runningStatusApplies = true;
				savedByte = statusByte;
				statusByte = helper.runningStatusByte;
			}else{
				throw new MidiFileException("corrupt MIDI file: status byte missing");
			}
		}
		
		int type = getType(statusByte);
		if(type == STATUS_ONE_BYTE){
			int data = 0;
			if (runningStatusApplies){
				data = savedByte;
			}else{
				data = readUnsignedByte(in, helper);
				helper.runningStatusByte = statusByte;
			}
			
			return new MidiEvent(MidiMessage.shortMessage((statusByte & 0xF0),(statusByte & 0x0F) , data), helper.ticks);
		}else if(type == STATUS_TWO_BYTES){
			int	data1 = 0;
			if (runningStatusApplies){
				data1 = savedByte;
			}else{
				data1 = readUnsignedByte(in, helper);
				helper.runningStatusByte = statusByte;
			}
			
			return new MidiEvent(MidiMessage.shortMessage((statusByte & 0xF0),(statusByte & 0x0F) , data1, readUnsignedByte(in, helper)), helper.ticks);
		}else if(type == STATUS_SYSEX){
			if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX){
				helper.runningStatusByte = -1;
			}
			int dataLength = (int) readVariableLengthQuantity(in, helper);
			byte[] data = new byte[dataLength];
			for (int i = 0; i < dataLength; i++){
				data[i] = (byte) readUnsignedByte(in, helper);
			}
		}else if(type == STATUS_META){
			if (CANCEL_RUNNING_STATUS_ON_META_AND_SYSEX){
				helper.runningStatusByte = -1;
			}
			int typeByte = readUnsignedByte(in, helper);
			int dataLength = (int) readVariableLengthQuantity(in, helper);
			byte[] data = new byte[dataLength];
			for (int i = 0; i < dataLength; i++){
				data[i] = (byte) readUnsignedByte(in, helper);
			}
			
			return new MidiEvent(MidiMessage.metaMessage(typeByte, data), helper.ticks);
		}
		
		return null;
	}
	
	private static int getType(int statusByte){
		if (statusByte < 0xf0) {
			int command = statusByte & 0xf0;
			if(command == 0x80 || command == 0x90 || command == 0xa0 || command == 0xb0 || command == 0xe0){
				return STATUS_TWO_BYTES;
			}
			else if(command == 0xc0 || command == 0xd0){
				return STATUS_ONE_BYTE;
			}
			return STATUS_NONE;
		}
		else if (statusByte == 0xf0 || statusByte == 0xf7){
			return STATUS_SYSEX;
		}
		else if (statusByte == 0xff){
			return STATUS_META;
		}
		else{
			return STATUS_NONE;
		}
	}
	
	public static long readVariableLengthQuantity(DataInputStream in, MidiTrackReaderHelper helper)throws MidiFileException, IOException{
		int	count = 0;
		long value = 0;
		while (count < 4){
			int	data = readUnsignedByte(in, helper);
			count++;
			value <<= 7;
			value |= (data & 0x7f);
			if (data < 128){
				return value;
			}
		}
		throw new MidiFileException("not a MIDI file: unterminated variable-length quantity");
	}
	
	public static int readUnsignedByte(DataInputStream dataInputStream, MidiTrackReaderHelper helper)throws IOException{
		helper.remainingBytes--;
		return dataInputStream.readUnsignedByte();
	}
	
	private class MidiTrackReaderHelper{
		protected long ticks = 0;
		protected long remainingBytes;
		protected int runningStatusByte;
		
		protected MidiTrackReaderHelper(long ticks,long remainingBytes,int runningStatusByte){
			this.ticks = ticks;
			this.remainingBytes = remainingBytes;
			this.runningStatusByte = runningStatusByte;
		}
	}
}
