package com.ibxm;

import java.io.*;

public class IBXM
{
	public static final String VERSION = "ibxm alpha 51 (c)2008 mumart@gmail.com";

	public static final int FP_SHIFT = 15;
	public static final int FP_ONE = 1 << FP_SHIFT;
	public static final int FP_MASK = FP_ONE - 1;

	private int sampling_rate, resampling_quality, volume_ramp_length;
	private int tick_length_samples, current_tick_samples;
	private int[] mixing_buffer, volume_ramp_buffer;

	private Module module;
	private Channel[] channels;
	private int[] global_volume, note;
	private int current_sequence_index, next_sequence_index;
	private int current_row, next_row;
	private int tick_counter, ticks_per_row;
	private int pattern_loop_count, pattern_loop_channel;

	public IBXM(InputStream input, int sample_rate, int quality) throws IOException
	{
		//System.out.println(VERSION);
		
		if(sample_rate < 8000)
		{
			sample_rate = 8000;
		}
		
		sampling_rate = sample_rate;
		volume_ramp_length = sampling_rate >> 10;
		volume_ramp_buffer = new int[volume_ramp_length*2];
		mixing_buffer = new int[sampling_rate/6];
		global_volume = new int[1];
		note = new int[5];
		
		set_module(load_module(input));
		set_resampling_quality(quality);
	}

	public void set_module(Module m)
	{
		int channel_idx;
		module = m;
		channels = new Channel[module.get_num_channels()];
		for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
		{
			channels[channel_idx] = new Channel(module, sampling_rate, global_volume);
		}
		set_sequence_index(0, 0);
	}

	public void set_resampling_quality(int quality)
	{
		resampling_quality = quality;
	}

	public int calculate_song_duration()
	{
		int song_duration;
		set_sequence_index(0, 0);
		next_tick();
		song_duration = tick_length_samples;
		while(!next_tick())
		{
			song_duration += tick_length_samples;
		}
		set_sequence_index(0, 0);
		return song_duration;
	}

	public void set_sequence_index(int sequence_index, int row)
	{
		int channel_idx;
		global_volume[0] = 64;
		for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
		{
			channels[channel_idx].reset();
			channels[channel_idx].set_panning(module.get_initial_panning(channel_idx));
		}
		set_global_volume(module.global_volume);
		set_speed(6);
		set_speed(module.default_speed);
		set_tempo(125);
		set_tempo(module.default_tempo);
		pattern_loop_count = -1;
		next_sequence_index = sequence_index;
		next_row = row;
		tick_counter = 0;
		current_tick_samples = tick_length_samples;
		clear_vol_ramp_buffer();
	}

	public void seek(int sample_position)
	{
		int idx;
		set_sequence_index(0, 0);
		next_tick();
		while(sample_position > tick_length_samples)
		{
			sample_position -= tick_length_samples;
			next_tick();
		}
		mix_tick();
		current_tick_samples = sample_position;
	}

	public int get_audio(byte[] output_buffer, int offset, int frames)
	{
		int output_idx, mix_idx, mix_end, count, amplitude;

		output_idx = offset;

		while(frames > 0)
		{
			count = tick_length_samples - current_tick_samples;

			if(count > frames)
			{
				count = frames;
			}
			
			mix_idx = current_tick_samples << 1;
			mix_end = mix_idx + (count << 1) - 1;

			while(mix_idx <= mix_end)
			{
				amplitude = mixing_buffer[mix_idx];

				if(amplitude > 32767)
				{
					amplitude = 32767;
				}

				if(amplitude < -32768)
				{
					amplitude = -32768;
				}

				output_buffer[output_idx] = (byte)(amplitude & 0xFF);
				output_buffer[output_idx + 1] = (byte)(amplitude >> 8);

				output_idx += 2;
				mix_idx += 1;
			}

			current_tick_samples = mix_idx >> 1;
			frames -= count;

			if(frames > 0)
			{
				next_tick();
				mix_tick();
				current_tick_samples = 0;
			}
		}
		
		return output_idx - offset;
	}

	private void mix_tick()
	{
		int channel_idx, mix_idx, mix_len;
		mix_idx = 0;
		mix_len = tick_length_samples + volume_ramp_length << 1;
		while(mix_idx < mix_len)
		{
			mixing_buffer[mix_idx] = 0;
			mix_idx += 1;
		}
		for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
		{
			mix_len = tick_length_samples + volume_ramp_length;
			channels[channel_idx].resample(mixing_buffer, 0, mix_len, resampling_quality);
		}
		volume_ramp();
	}

	private boolean next_tick()
	{
		int channel_idx;
		boolean song_end;
		for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
		{
			channels[channel_idx].update_sample_idx(tick_length_samples);
		}
		tick_counter -= 1;
		if(tick_counter <= 0)
		{
			tick_counter = ticks_per_row;
			song_end = next_row();
		}
		else
		{
			for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
			{
				channels[channel_idx].tick();
			}
			song_end = false;
		}
		return song_end;
	}

	private boolean next_row()
	{
		int channel_idx, effect, effect_param;
		boolean song_end;
		Pattern pattern;
		song_end = false;
		if(next_sequence_index < 0)
		{
			/* Bad next sequence index.*/
			next_sequence_index = 0;
			next_row = 0;
		}
		if(next_sequence_index >= module.get_sequence_length())
		{
			/* End of sequence.*/
			song_end = true;
			next_sequence_index = module.restart_sequence_index;
			if(next_sequence_index < 0)
			{
				next_sequence_index = 0;
			}
			if(next_sequence_index >= module.get_sequence_length())
			{
				next_sequence_index = 0;
			}
			next_row = 0;
		}
		if(next_sequence_index < current_sequence_index)
		{
			/* Jump to previous pattern. */
			song_end = true;
		}
		if(next_sequence_index == current_sequence_index)
		{
			if(next_row <= current_row)
			{
				if(pattern_loop_count < 0)
				{
					/* Jump to previous row in the same pattern, but not a pattern loop. */
					song_end = true;
				}
			}
		}
		current_sequence_index = next_sequence_index;
		pattern = module.get_pattern_from_sequence(current_sequence_index);
		if(next_row < 0 || next_row >= pattern.num_rows)
		{
			/* Bad next row.*/
			next_row = 0;
		}
		current_row = next_row;
		next_row = current_row + 1;
		if(next_row >= pattern.num_rows)
		{
			next_sequence_index = current_sequence_index + 1;
			next_row = 0;
		}
		for(channel_idx = 0; channel_idx < channels.length; channel_idx++)
		{
			pattern.get_note(note, current_row * channels.length + channel_idx);
			effect = note[3];
			effect_param = note[4];
			channels[channel_idx].row(note[0], note[1], note[2], effect, effect_param);
			switch(effect)
			{
				case 0x0B:
					/* Pattern Jump.*/
					if(pattern_loop_count < 0)
					{
						next_sequence_index = effect_param;
						next_row = 0;
					}
					break;
				case 0x0D:
					/* Pattern Break.*/
					if(pattern_loop_count < 0)
					{
						next_sequence_index = current_sequence_index + 1;
						next_row = (effect_param >> 4) * 10 + (effect_param & 0x0F);
					}
					break;
				case 0x0E:
					/* Extended.*/
					switch(effect_param & 0xF0)
					{
						case 0x60:
							/* Pattern loop.*/
							if((effect_param & 0x0F) == 0)
							{
								/* Set loop marker on this channel. */
								channels[channel_idx].pattern_loop_row = current_row;
							}
							if(channels[channel_idx].pattern_loop_row < current_row)
							{
								/* Marker and parameter are valid. Begin looping. */
								if(pattern_loop_count < 0)
								{
									/* Not already looping, begin. */
									pattern_loop_count = effect_param & 0x0F;
									pattern_loop_channel = channel_idx;
								}
								if(pattern_loop_channel == channel_idx)
								{
									/* Loop in progress on this channel. Next iteration. */
									if(pattern_loop_count == 0)
									{
										/* Loop finished. */
										/* Invalidate current marker. */
										channels[channel_idx].pattern_loop_row = current_row + 1;
									}
									else
									{
										/* Count must be higher than zero. */
										/* Loop and cancel any breaks on this row. */
										next_row = channels[channel_idx].pattern_loop_row;
										next_sequence_index = current_sequence_index;
									}
									pattern_loop_count -= 1;
								}
							}
							break;
						case 0xE0:
							/* Pattern delay.*/
							tick_counter += ticks_per_row * (effect_param & 0x0F);
							break;
					}
					break;
				case 0x0F:
					/* Set Speed/Tempo.*/
					if(effect_param < 32)
					{
						set_speed(effect_param);
						tick_counter = ticks_per_row;
					}
					else
					{
						set_tempo(effect_param);
					}
					break;
				case 0x25:
					/* S3M Set Speed.*/
					set_speed(effect_param);
					tick_counter = ticks_per_row;
					break;
			}
		}
		return song_end;
	}

	private void set_global_volume(int volume)
	{
		if(volume < 0)
		{
			volume = 0;
		}
		if(volume > 64)
		{
			volume = 64;
		}
		global_volume[0] = volume;
	}

	private void set_speed(int speed)
	{
		if(speed > 0 && speed < 256)
		{
			ticks_per_row = speed;
		}
	}

	private void set_tempo(int bpm)
	{
		if(bpm > 31 && bpm < 256)
		{
			tick_length_samples = (sampling_rate * 5) / (bpm * 2);
		}
	}

	private void volume_ramp()
	{
		int ramp_idx, next_idx, ramp_end;
		int volume_ramp_delta, volume, sample;
		sample = 0;
		volume_ramp_delta = FP_ONE / volume_ramp_length;
		volume = 0;
		ramp_idx = 0;
		next_idx = 2 * tick_length_samples;
		ramp_end = volume_ramp_length * 2 - 1;
		while(ramp_idx <= ramp_end)
		{
			sample = volume_ramp_buffer[ramp_idx] * (FP_ONE - volume) >> FP_SHIFT;
			mixing_buffer[ramp_idx] = sample + (mixing_buffer[ramp_idx] * volume >> FP_SHIFT);
			volume_ramp_buffer[ramp_idx] = mixing_buffer[next_idx + ramp_idx];
			sample = volume_ramp_buffer[ramp_idx + 1] * (FP_ONE - volume) >> FP_SHIFT;
			mixing_buffer[ramp_idx + 1] = sample + (mixing_buffer[ramp_idx + 1] * volume >> FP_SHIFT);
			volume_ramp_buffer[ramp_idx + 1] = mixing_buffer[next_idx + ramp_idx + 1];
			volume += volume_ramp_delta;
			ramp_idx += 2;
		}
	}

	private void clear_vol_ramp_buffer()
	{
		int ramp_idx, ramp_end;
		ramp_idx = 0;
		ramp_end = volume_ramp_length * 2 - 1;
		while(ramp_idx <= ramp_end)
		{
			volume_ramp_buffer[ramp_idx] = 0;
			ramp_idx += 1;
		}
	}
	
	public String getSongTitle()
	{
		return module.song_title;
	}
	
	/**
		Decode the data in the specified InputStream into a Module instance.
		@param input an InputStream containing the module file to be decoded.
		@throws IllegalArgumentException if the data is not recognised as a module file.
	*/
	public static Module load_module(InputStream input) throws IllegalArgumentException, IOException
	{
		DataInputStream data_input_stream = new DataInputStream(input);
		
		/* Check if data is in XM format.*/
		byte[] xm_header = new byte[60];
		
		data_input_stream.readFully(xm_header);
		
		if(FastTracker2.is_xm(xm_header))
		{
			return FastTracker2.load_xm(xm_header, data_input_stream);
		}
		
		/* Check if data is in ScreamTracker 3 format.*/
		byte[] s3m_header = new byte[96];
		
		System.arraycopy(xm_header, 0, s3m_header, 0, 60);
		data_input_stream.readFully(s3m_header, 60, 36);
		
		if(ScreamTracker3.is_s3m(s3m_header))
		{
			return ScreamTracker3.load_s3m(s3m_header, data_input_stream);
		}
		
		/* Check if data is in ProTracker format.*/
		byte[] mod_header = new byte[1084];
		
		System.arraycopy(s3m_header, 0, mod_header, 0, 96);
		data_input_stream.readFully(mod_header, 96, 988);
		
		return ProTracker.load_mod(mod_header, data_input_stream);
	}
}

