package theGhastModding.midiPlayer.gui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.filechooser.FileNameExtensionFilter;

import jouvieje.bass.structures.HSOUNDFONT;
import theGhastModding.midiPlayer.main.InsertionSort;
import theGhastModding.midiPlayer.main.TheGhastMidiPlayerMain;
import theGhastModding.midiPlayer.midi.KeyState;
import theGhastModding.midiPlayer.midi.MIDIEvent;
import theGhastModding.midiPlayer.midi.MIDILoader;
import theGhastModding.midiPlayer.midi.Note;
import theGhastModding.midiPlayer.midi.NoteOff;
import theGhastModding.midiPlayer.midi.NoteOn;
import theGhastModding.midiPlayer.midi.ProgramChangeEvent;
import theGhastModding.midiPlayer.midi.Slice;
import theGhastModding.midiPlayer.midi.TempoEvent;
import theGhastModding.midiPlayer.midi.Track;
import theGhastModding.midiPlayer.resources.Textures;
import theGhastModding.synthesizer.main.MidiEvent;
import theGhastModding.synthesizer.main.TGMSynthesizer;

@SuppressWarnings("serial")
public class TheGhastMidiPlayerPanel extends JPanel implements Runnable, WindowListener {
	
	private BufferedImage image;
	private Graphics2D imageGraphics;
	private boolean running = false;
	private static final int FPS = 64;
	private Thread thread;
	private JMenuItem openMidiItem,exitItem;
	private JFileChooser midiChooser;
	private List<MIDIEvent> otherEvents = null;
	private List<MIDIEvent> playbackOtherEvents = null;
	private long midiLength;
	private long resolution;
	private long noteCount;
	private Thread midiLoaderThread = null;
	private JMenu fileMenu;
	private JMenu helpMenu;
	private AboutDialog about;
	private JPanel panel;
	private JSlider slider;
	private JButton playButton, pauseButton, stopButton = null;
	private int voiceLimit = 500;
	private List<Color> trackColors;
	private List<BufferedImage> noteTrackImages;
	private List<BufferedImage> coloredKeyboardTexturesWhite;
	private List<BufferedImage> coloredKeyboardTexturesWhite2;
	private List<BufferedImage> coloredKeyboardTexturesBlack;
	private KeyState[] keyStates;
	public static boolean[] isWhiteKey = new boolean[]{
			true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,
			false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,
			false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,
			false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,
			true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,
			false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,
			false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,
			false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,true,true,false,true,false,true,false,true,true,false,true,false,
			true,true,false,true,false,true,false,true
		};
	private boolean playback;
	private JLabel fpsLabel;
	private List<Slice> slices;
	private float TPS;
	private boolean paused = false;
	private long timerThen;
	private long timerNow;
	private double tickPosition;
	private JSlider speedSlider;
	private int usedVoices;
	private int trackCount;
	private Random rnd = new Random();
	private SettingsDialog settings;
	private JMenu preferencesMenu;
	private List<TempoEvent> playbackTempos;
	private JMenu midiItem;
	private HSOUNDFONT sf = null;
	private boolean sound = true;
	private Clip soundfile = null;
	private JSpinner voicesSpinner;
	private JMenuItem sfItem;
	private Receiver receiver;
	private MidiDevice device;
	private int cores = 8;
	private RenderThread[] rts;
	private ThreadPoolExecutor threadPool;
	private double keyLength;
	public static boolean largeKeyboard = false;
	private FPSWindow fpsWindow;
	private int[][] notesPlaying;

	public TheGhastMidiPlayerPanel(int cores){
		super();
		this.cores = cores;
		setLayout(null);
		panel = new JPanel();
		panel.setLayout(null);
		panel.setBounds(0, 60, frameWidth, frameHeight);
		add(panel);
		image = new BufferedImage(1280, 720, BufferedImage.TYPE_INT_RGB);
		imageGraphics = (Graphics2D)image.getGraphics();
		setPreferredSize(new Dimension((TheGhastMidiPlayerMain.smaller ? 960 : 1280), (TheGhastMidiPlayerMain.smaller ? 540 : 720)));
		midiChooser = new JFileChooser();
		midiChooser.setDialogTitle("Select a MIDI to open");
		midiChooser.setFileFilter(new FileNameExtensionFilter("MIDI files", "mid", "midi", "MID", "MIDI"));
		JMenuBar bar = new JMenuBar();
		TheGhastMidiPlayerMain.frame.setJMenuBar(bar);
		fileMenu = new JMenu("File");
		TheGhastMidiPlayerMain.frame.addWindowListener(this);
		bar.add(fileMenu);
		midiItem = new JMenu("MIDI");
		fileMenu.add(midiItem);
		openMidiItem = new JMenuItem("Open MIDI");
		openMidiItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				loadMidi();
			}
		});
		midiItem.add(openMidiItem);
		JMenuItem closeMidiItem = new JMenuItem("Close MIDI");
		closeMidiItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				if(slices != null){
					slices.clear();
				}
				slices = null;
				if(otherEvents != null){
					otherEvents.clear();
				}
				otherEvents = null;
				System.gc();
			}
		});
		midiItem.add(closeMidiItem);
		exitItem = new JMenuItem("Exit");
		exitItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				running = false;
				try {
					if(TGMSynthesizer.isSynthStarted()){
						TGMSynthesizer.stopSynth();
					}
					if(fpsWindow != null && fpsWindow.isVisible()) fpsWindow.setVisible(false);
				}catch(Exception e2){
					e2.printStackTrace();
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error stopping TGM's synthesizer: " + e2.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
					System.exit(1);
				}
				System.exit(0);
			}
		});
		fileMenu.add(exitItem);
		preferencesMenu = new JMenu("Preferences");
		JMenuItem settingsItem = new JMenuItem("Settings");
		settings = new SettingsDialog();
		settingsItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				settings.setVisible(true);
			}
		});
		preferencesMenu.add(settingsItem);
		JFileChooser sfChooser = new JFileChooser();
		sfChooser.setFileFilter(new FileNameExtensionFilter("Soundfonts", "sf2", "SF2", "sfz", "SFZ"));
		sfChooser.setDialogTitle("Select a soundfont to load");
		sfItem = new JMenuItem("Change soundfont");
		sfItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				if(!TGMSynthesizer.isSynthStarted()){
					return;
				}
				if(sfChooser.showOpenDialog(TheGhastMidiPlayerMain.frame) == JFileChooser.APPROVE_OPTION){
					if(!sfChooser.getSelectedFile().exists()){
						JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "The selected file doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
						return;
					}
					try {
						if(sf != null){
							TGMSynthesizer.unloadFont(sf);
						}
					}catch(Exception e2){
						e2.printStackTrace();
						JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error unloading old soundfont", "Error", JOptionPane.ERROR_MESSAGE);
						return;
					}
					try {
						sf = TGMSynthesizer.loadFont(sfChooser.getSelectedFile().getPath());
					}catch(Exception e2){
						e2.printStackTrace();
						JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error loading new soundfont", "Error", JOptionPane.ERROR_MESSAGE);
						return;
					}
				}
			}
		});
		preferencesMenu.add(sfItem);
		bar.add(preferencesMenu);
		helpMenu = new JMenu("Help");
		bar.add(helpMenu);
		JMenuItem helpItem = new JMenuItem("Download help file");
		//https://www.dropbox.com/s/8xw11uiapwuctrp/help.txt?dl=0
		helpItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				try {
		        	URL website = new URL("https://www.dropbox.com/s/8xw11uiapwuctrp/help.txt?dl=1");
		        	ReadableByteChannel rbc = Channels.newChannel(website.openStream());
		        	FileOutputStream fos = new FileOutputStream("help.txt");
		        	fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
		        	fos.close();
				}catch(Exception e2){
					e2.printStackTrace();
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error downloading help file", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				try {
					if(Desktop.isDesktopSupported()){
						Desktop.getDesktop().open(new File("help.txt"));
					}else{
						JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Can't automatically open file. Please open the file \"help.txt\" in the applications installation directory", "Error", JOptionPane.ERROR_MESSAGE);
					}
				}catch(Exception e3){
					e3.printStackTrace();
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error opening help file", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
			}
		});
		helpMenu.add(helpItem);
		JMenuItem aboutItem = new JMenuItem("About");
		about = new AboutDialog();
		aboutItem.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				about.setVisible(true);
			}
		});
		helpMenu.add(aboutItem);
		slider = new JSlider();
		slider.setMinimum(0);
		slider.setMaximum(1000);
		slider.setEnabled(false);
		slider.setValue(0);
		slider.setBounds(5, 35, 1270, 16);
		add(slider);
		speedSlider = new JSlider();
		speedSlider.setMajorTickSpacing(200);
		speedSlider.setMinorTickSpacing(100);
		speedSlider.setPaintTicks(true);
		speedSlider.setMaximum(200);
		speedSlider.setMinimum(0);
		speedSlider.setValue(100);
		speedSlider.setBounds(145, 5, 400, 20);
		add(speedSlider);
		JCheckBox disableSound = new JCheckBox("Disable sound");
		disableSound.setToolTipText("Enabeling sound can make you loose AL LOT of FPS in spammy parts so turn this off before playing a heavy MIDI and use a soundfile");
		disableSound.setBounds(550, 5, 100, 20);
		disableSound.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				sound = !disableSound.isSelected();
				if(sound == false){
					noteOffToAllChannels();
				}else{
					if(soundfile != null){
						if(JOptionPane.showConfirmDialog(TheGhastMidiPlayerMain.frame, "Re-enabling sound will unload the loaded soundfile. Continue?", "LOL",JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE) == 0){
							soundfile.stop();
							soundfile.close();
							soundfile = null;
						}else{
							disableSound.setSelected(true);
							sound = false;
						}
					}
				}
			}
		});
		add(disableSound);
		JButton loadSoundfile = new JButton("Load a soundfile instead");
		loadSoundfile.setBounds(650, 5, 190, 20);
		JFileChooser soundChooser = new JFileChooser();
		soundChooser.setDialogTitle("Select a .wav to load (MUST BE 16BIT!)");
		soundChooser.setFileFilter(new FileNameExtensionFilter("WAV files", "wav", "WAV"));
		loadSoundfile.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				if(sound){
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "This only works with sound disabled", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				if(soundChooser.showOpenDialog(TheGhastMidiPlayerMain.frame) == JFileChooser.APPROVE_OPTION){
					if(!soundChooser.getSelectedFile().exists()){
						JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "The selected file doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
						return;
					}
				    try {
				        InputStream audioSrc = new FileInputStream(soundChooser.getSelectedFile());
				        InputStream bufferedIn = new BufferedInputStream(audioSrc);
				    	   AudioInputStream stream = AudioSystem.getAudioInputStream(bufferedIn);
				    	    AudioFormat format = stream.getFormat();
				    	    DataLine.Info info = new DataLine.Info(Clip.class, format);
				    	    soundfile = (Clip) AudioSystem.getLine(info);
				    	    soundfile.open(stream);
				    } catch(Exception ex) {
				    	ex.printStackTrace();
				    	JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error loading soundfile: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
				    	soundfile = null;
				    	return;
				    }
				}
			}
		});
		add(loadSoundfile);
		JLabel speedLabel = new JLabel("Speed:");
		speedLabel.setBounds(110, 4, 35, 20);
		add(speedLabel);
		playButton = new JButton(new ImageIcon(Textures.play));
		playButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				if(playback){
					if(paused){
						paused = false;
						playButton.setEnabled(false);
						pauseButton.setEnabled(true);
						if(soundfile != null){
							soundfile.start();
						}
					}
					return;
				}
				if(slices == null || slices.isEmpty()){
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "There is no MIDI loaded to play", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				startPlayback();
			}
		});
		playButton.setBounds(5, 4, 20, 20);
		add(playButton);
		pauseButton = new JButton(new ImageIcon(Textures.pause));
		pauseButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent arg0) {
				if(!paused){
					paused = true;
					pauseButton.setEnabled(false);
					playButton.setEnabled(true);
					if(soundfile != null){
						soundfile.stop();
					}else{
						try { Thread.sleep(10); } catch(Exception e){e.printStackTrace();}
						noteOffToAllChannels();
					}
				}
			}
		});
		pauseButton.setEnabled(false);
		pauseButton.setBounds(25, 4, 20, 20);
		add(pauseButton);
		stopButton = new JButton(new ImageIcon(Textures.stop));
		stopButton.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent e) {
				stopPlayback();
			}
		});
		stopButton.setEnabled(false);
		stopButton.setBounds(45, 4, 20, 20);
		add(stopButton);
		
		voicesSpinner = new JSpinner();
		voicesSpinner.addChangeListener(new ChangeListener(){
			@Override
			public void stateChanged(ChangeEvent e) {
				if(!TGMSynthesizer.isSynthStarted()){
					return;
				}
				voiceLimit = Integer.parseInt(voicesSpinner.getValue().toString());
				//lblCurrentVoices.setText("Currently used voices: " + Integer.toString(usedVoices) + "/" + Integer.toString(voiceLimit));
				try {
					TGMSynthesizer.setMaxVoices(Integer.parseInt(voicesSpinner.getValue().toString()));
				}catch(Exception e2){
					e2.printStackTrace();
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error changing voice limit", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
			}
		});
		voicesSpinner.setModel(new SpinnerNumberModel(500, 1, 10000, 1));
		voicesSpinner.setBounds(1018, 4, 71, 20);
		add(voicesSpinner);
		lblVoiceLimit = new JLabel("Voice limit:");
		lblVoiceLimit.setBounds(964, 7, 56, 14);
		add(lblVoiceLimit);
		lblCurrentVoices = new JLabel("Currently used voices: 0/500");
		lblCurrentVoices.setBounds(1099, 7, 176, 14);
		add(lblCurrentVoices);
		fpsLabel = new JLabel("FPS: 0");
		fpsLabel.setBounds(65, 7, 176, 14);
		add(fpsLabel);
		
		keyStates = new KeyState[largeKeyboard ? 256 : 128];
		for(int i = 0; i < (largeKeyboard ? 256 : 128); i++){
			keyStates[i] = new KeyState();
		}
		if(TheGhastMidiPlayerMain.smaller){
			for(Component c:this.getComponents()){
				Rectangle r = c.getBounds();
				c.setBounds(r.x - r.x / 4, r.y - r.y / 4, r.width - r.width / 4, r.height - r.height / 4);
			}
		}
		TheGhastMidiPlayerMain.frame.setIconImage(Textures.icon);
		thread = new Thread(this);
		thread.start();
	}
	
	private void noteOffToAllChannels(){
		if(TGMSynthesizer.isSynthStarted()){
			try {
				for(int i = 0; i < 16; i++){
					TGMSynthesizer.sendEvent(new MidiEvent(MidiEvent.MIDI_EVENT_NOTESOFF, i ,0,0));
				}
			}catch(Exception e){
				e.printStackTrace();
				JOptionPane.showMessageDialog(this, "Error sending NoteOff to all channels", "Error", JOptionPane.ERROR_MESSAGE);
				return;
			}
		}
		if(SettingsDialog.useExternalSynth){
			for(int i = 0; i < 16; i++){
				for(int j = 0; j < 128; j++){
					sendEventToExternalSynth(0x80, i, j, 0);
				}
			}
		}
	}
	
	private ShortMessage message = new ShortMessage();
	private void sendEventToExternalSynth(int command, int channel, int data1, int data2){
		if((command == 0x80 || command == 0x90) && data1 >= 128) return;
		if(device == null) return;
		if(!device.isOpen()){
			return;
		}
		try {
			if(channel == -1){
				message.setMessage(command, data1, data2);
			}else if(data1 == -1 && data2 == -1 && channel == -1){
				message.setMessage(command);
			}else{
				message.setMessage(command + channel, channel, data1, data2);
			}
			receiver.send(message, 0);
		} catch(Exception e){
			System.err.println("Error sending MIDI event");
			e.printStackTrace();
			return;
		}
	}
	
	private void startPlayback(){
		settings.lockMidiSettings();
		if(playbackTempos != null){
			playbackTempos.clear();
			playbackTempos = null;
		}
		if(SettingsDialog.useExternalSynth){
			device = SettingsDialog.availableDevices.get(SettingsDialog.selectedDevice - 1);
			try {
				if(!device.isOpen()){
					device.open();
				}
				receiver = device.getReceiver();
			} catch(Exception e){
				e.printStackTrace();
				JOptionPane.showMessageDialog(this, "Error opening MIDI device or getting receiver from external MIDI device", "Error", JOptionPane.ERROR_MESSAGE);
				settings.unlockMidiSettings();
				return;
			}
		}
		playbackTempos = new ArrayList<TempoEvent>();
		TempoEvent firstTempo = null;
		playbackOtherEvents = new ArrayList<MIDIEvent>();
		for(MIDIEvent event:otherEvents){
			if(event instanceof TempoEvent){
				playbackTempos.add((TempoEvent)event);
			}else{
				playbackOtherEvents.add(event);
			}
			if(event instanceof TempoEvent && event.getTick() == 0){
				firstTempo = (TempoEvent)event;
			}
		}
		if(firstTempo == null){
			JOptionPane.showMessageDialog(this, "Error starting MIDI playback. No tempo event found at tick 0", "Error", JOptionPane.ERROR_MESSAGE);
			return;
		}
		noteTrackImages = new ArrayList<BufferedImage>();
		coloredKeyboardTexturesWhite = new ArrayList<BufferedImage>();
		coloredKeyboardTexturesWhite2 = new ArrayList<BufferedImage>();
		coloredKeyboardTexturesBlack = new ArrayList<BufferedImage>();
		if(trackColors != null){
			trackColors.clear();
		}
		trackColors = new ArrayList<Color>();
		for(Color c:SettingsDialog.trackColours){
			if(SettingsDialog.transparentNoets){
				trackColors.add(new Color(c.getRed(), c.getGreen(), c.getBlue(), 50));
			}else{
				trackColors.add(new Color(c.getRed(), c.getGreen(), c.getBlue()));
			}
		}
		if(SettingsDialog.channelColoring){
			for(int i = 0; i < 20; i++){
				Color c = null;
				if(SettingsDialog.transparentNoets){
					c = new Color(100 + rnd.nextInt(140),100 + rnd.nextInt(140),100 + rnd.nextInt(140), 50);
				}else{
					c = new Color(100 + rnd.nextInt(140),100 + rnd.nextInt(140),100 + rnd.nextInt(140));
				}
				if(c.getRed() < 200 && c.getGreen() < 200 && c.getBlue() < 200){
					i--;
					continue;
				}
				trackColors.add(c);
			}
		}else{
			for(int i = 0; i < trackCount + 2; i++){
				Color c = null;
				if(SettingsDialog.transparentNoets){
					c = new Color(100 + rnd.nextInt(140),100 + rnd.nextInt(140),100 + rnd.nextInt(140), 50);
				}else{
					c = new Color(100 + rnd.nextInt(140),100 + rnd.nextInt(140),100 + rnd.nextInt(140));
				}
				if(c.getRed() < 200 && c.getGreen() < 200 && c.getBlue() < 200){
					i--;
					continue;
				}
				trackColors.add(c);
			}
		}
		for(Color c:trackColors){
			noteTrackImages.add(colorImage(c, Textures.note));
			coloredKeyboardTexturesWhite.add(colorImage(c, Textures.whitepressed));
			coloredKeyboardTexturesWhite2.add(colorImage(c, Textures.whitepressed2));
			coloredKeyboardTexturesBlack.add(colorImage(c, Textures.blackpressed));
		}
		notesPlaying = new int[16][(largeKeyboard ? 256 : 128)];
		midiItem.setEnabled(false);
		playButton.setEnabled(false);
		stopButton.setEnabled(true);
		pauseButton.setEnabled(true);
		TPS = (firstTempo.getBpm() / 60) * resolution;
		this.midiLength += (long)TPS;
		tickPosition = 0;
		if(!sound && soundfile != null){
			soundfile.setMicrosecondPosition(0);
			soundfile.start();
		}
		for(Slice s:slices){
			for(int i = 0; i < cores; i++) {
				for(Note n:s.getNotesForCore(i)){
					n.setOffPlayed(false);
					n.setOnPlayed(false);
				}
			}
		}
		System.gc();
		timerNow = timerThen = System.nanoTime();
		playback = true;
	}
	
	public BufferedImage colorImage(Color c, BufferedImage toColor){
		BufferedImage tempImage = new BufferedImage(toColor.getWidth(), toColor.getHeight(), BufferedImage.TYPE_INT_ARGB);
		Graphics tempGraphics = tempImage.getGraphics();
		for(int i = 0; i < tempImage.getWidth(); i++){
			for(int j = 0; j < tempImage.getHeight(); j++){
				Color c2 = new Color(toColor.getRGB(i, j), true);
				int r = c.getRed() - (255 - c2.getRed());
				int g = c.getGreen() - (255 - c2.getGreen());
				int b = c.getBlue() - (255 - c2.getBlue());
				int alpha = c2.getAlpha();
				//System.out.println(r + "," + g + "," + b);
				if(r < 0){
					r = 0;
				}
				if(g < 0){
					g = 0;
				}
				if(b < 0){
					b = 0;
				}
				tempGraphics.setColor(new Color(r,g,b,alpha));
				tempGraphics.fillRect(i, j, 1, 1);
			}
		}
		tempGraphics.dispose();
		return tempImage;
	}
	
	private void stopPlayback(){
		if(soundfile != null){
			soundfile.stop();
		}
		playback = false;
		paused = false;
		playButton.setEnabled(true);
		pauseButton.setEnabled(false);
		stopButton.setEnabled(false);
		midiItem.setEnabled(true);
		slider.setValue(0);
		settings.unlockMidiSettings();
		try {
			if(SettingsDialog.useExternalSynth){
				if(device.isOpen()){
					receiver.close();
					device.close();
				}
			}
		}catch(Exception e){
			e.printStackTrace();
			JOptionPane.showMessageDialog(null, "Error closing external synthesizer and its receiver", "Error", JOptionPane.ERROR_MESSAGE);
		}
		try { Thread.sleep(10); } catch(Exception e){ e.printStackTrace(); }
		for(KeyState ks:keyStates){
			ks.setIsPressed(false);
			ks.pressedTracks().clear();
		}
		noteOffToAllChannels();
	}
	
	private int secondCounter = 0;
	
	@Override
	public void run() {
		fpsWindow = new FPSWindow();
		fpsWindow.setVisible(true);
		running = true;
		double frameTime = 1000000000D / (double)FPS;
		long fpsTimer = System.nanoTime();
		keyboardHeight = (int)((double)frameHeight / 100D * 12.75D) + 10;
		blackKeyHeight = (int)((double)keyboardHeight / 100D * 63.125D);
		//if(TheGhastMidiPlayerMain.smaller){
			keyboardHeight += 30;
			blackKeyHeight -= 10;
		//}
		int fps = 0;
		long fpsTimer2 = System.currentTimeMillis();
		int lastFPS = 60;
		keyLength = (double)frameWidth / (largeKeyboard ? 256D : 128D) ;
		rts = new RenderThread[cores];
		this.threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(cores);
		for(int i = 0; i < cores; i++){
			rts[i] = new RenderThread(frameHeight, i);
		}
		while(running){
			if(lastFPS <= 30){
				draw();
				fps++;
			}else{
				if(System.nanoTime() - fpsTimer >= frameTime){
					fpsTimer = System.nanoTime();
					draw();
					fps++;
				}
			}
			if(System.currentTimeMillis() - fpsTimer2 >= 1000){
				if(playback && secondCounter < 15 && soundfile != null){
					//tickPosition = (float)soundfile.getMicrosecondPosition() / 1000000F * TPS;
					if(secondCounter == 12) soundfile.setMicrosecondPosition((long) (tickPosition / TPS * 1000000D));
					secondCounter++;
				}
				fpsTimer2 = System.currentTimeMillis();
				fpsLabel.setText("FPS: " + Integer.toString(fps));
				if(fpsWindow != null && fpsWindow.isVisible()) fpsWindow.setFPS(fps);
				lastFPS = fps;
				if(SettingsDialog.useExternalSynth && voicesSpinner.isEnabled()){
					voicesSpinner.setEnabled(false);
					sfItem.setEnabled(false);
					sf = null;
				}
				if(!SettingsDialog.useExternalSynth && !voicesSpinner.isEnabled()){
					voicesSpinner.setEnabled(true);
					sfItem.setEnabled(true);
				}
				if(SettingsDialog.useExternalSynth){
					voiceLimit = SettingsDialog.maxEventsToSynth;
				}
				fps = 0;
			}
			try{
				if(!playback){
					Thread.sleep(1);
				}
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}
	
	private void sendNoteToSynth(int key, int channel, int velocity){
		if(key >= 128 || key < 0) return;
		if(TGMSynthesizer.isSynthStarted()){
			try {
				TGMSynthesizer.sendEvent(new MidiEvent(MidiEvent.MIDI_EVENT_NOTE, channel, key, velocity));
			}catch(Exception e){
				System.err.println("Error sending note to synth");
				e.printStackTrace();
			}
		}
		if(SettingsDialog.useExternalSynth){
			sendEventToExternalSynth(velocity == 0 ? 0x80 : 0x90, channel, key, velocity);
		}
	}
    
	private Graphics windowGraphics;
	private JLabel lblVoiceLimit;
	private JLabel lblCurrentVoices;
	private final int frameWidth = 1280;
	private final int frameHeight = 720;
	private int keyboardHeight;
	private int blackKeyHeight;
	//private int sentNotes = 0;
	private List<Slice> aaa = new ArrayList<Slice>();
	private List<Future<?>> futures = new ArrayList<Future<?>>();
	
	private void draw(){
		imageGraphics.setColor(Color.BLACK);
		imageGraphics.fillRect(0, 0, frameWidth, frameHeight);
		try {
			if(TGMSynthesizer.isSynthStarted()){
				usedVoices = TGMSynthesizer.getActiveVoices();
			}
			if(SettingsDialog.useExternalSynth){
				usedVoices = -1;
			}
		}catch(Exception e){
			e.printStackTrace();
			System.err.println("Error getting used voices");
		}
		if(usedVoices >= 0){
			lblCurrentVoices.setText("Currently used voices: " + Integer.toString(usedVoices) + "/" + Integer.toString(voiceLimit));
		} else{
			lblCurrentVoices.setText("Using external synthesizer");
		}
		if(playback){
			//sentNotes = 0;
			timerNow = System.nanoTime();
			if(!paused){
				tickPosition += ((((double)timerNow - (double)timerThen)/1000000000D) * (TPS * ((double)speedSlider.getValue() / 100D)));
			}
			timerThen = timerNow;
			if((int) (tickPosition / midiLength * 100D) > 100){
				slider.setValue(100);
			}else{
				slider.setValue((int) (tickPosition / midiLength * 1000D));
			}
			if(tickPosition > midiLength){
				System.out.println(tickPosition + "," + midiLength);
				stopPlayback();
			}
			int toRemove = -1;
			for(int i = 0; i < playbackTempos.size(); i++){
				if(playbackTempos.get(i).getTick() <= tickPosition){
					TPS = (playbackTempos.get(i).getBpm() / 60) * resolution;
					if(toRemove == -1){
						toRemove = i;
					}
				}
			}
			if(toRemove >= 0){
				playbackTempos.remove(toRemove);
			}
			toRemove = -1;
			for(MIDIEvent e:playbackOtherEvents){
				if(e.getTick() <= tickPosition && e instanceof ProgramChangeEvent && toRemove == -1){
					if(TGMSynthesizer.isSynthStarted()){
						try {
							TGMSynthesizer.sendEvent(new MidiEvent(MidiEvent.MIDI_EVENT_PROGRAM, ((ProgramChangeEvent)e).getChannel(), ((ProgramChangeEvent)e).getProgram(), 0));
						} catch(Exception e2){
							System.err.println("Error sending event to synth");
							e2.printStackTrace();
						}
					}
					if(SettingsDialog.useExternalSynth){
						sendEventToExternalSynth(0xC0, ((ProgramChangeEvent)e).getChannel(), ((ProgramChangeEvent)e).getProgram(), 0);
					}
				}
			}
			if(toRemove >= 0){
				playbackOtherEvents.remove(toRemove);
			}
			aaa.clear();
			futures.clear();
			for(Slice s:slices){
				if(s.getStartTick() <= this.tickPosition + frameHeight && s.getStartTick() + s.longestNote() >= tickPosition - 2 * TPS){
						/*for(Note n:s.getNotes()){
							updateKeyStatesWithNote(n);
							renderNote(n, imageGraphics, 0);
						}*/
						//Multicore rendering happens here.
						//Known bugs: breaks with computers that have over 128 CPU cores
						aaa.add(s);
				}
			}
			for(int i = 0; i < cores; i++){
				rts[i].setSlices(aaa);
				rts[i].reset();
				futures.add(threadPool.submit(rts[i]));
			}
			try {
				Thread.sleep(1);
				for(Future<?> f:futures) {
					if(f.get() != null) while(f.get() != null) {}
				}
			} catch(Exception e) { e.printStackTrace(); }
			for(int i = cores - 1; i >= 0; i--){
				imageGraphics.drawImage(rts[i].partImage, (int)(keyLength * (double)rts[i].nStart), 0, rts[i].partImage.getWidth(), rts[i].partImage.getHeight(), null);
				//try { ImageIO.write(rts[i].getPartImage(), "png", new File(Long.toString(System.nanoTime()) + "." + Integer.toString(i) + ".png")); } catch(Exception e){e.printStackTrace();}
			}
		}
		if(SettingsDialog.fancyKeyboard || largeKeyboard){
			for(int i = 0; i < (largeKeyboard ? 256 : 128); i++){
				if(isWhiteKey[i]){
					if(!isWhiteKey[i + 1]){
						if(keyStates[i].isPressed()){
							imageGraphics.drawImage(coloredKeyboardTexturesWhite2.get(keyStates[i].pressedTracks().get(keyStates[i].pressedTracks().size() - 1)), (int)keyLength * i - (int)(keyLength / 2D), frameHeight - keyboardHeight, (int)keyLength + (int)keyLength, keyboardHeight, null);
						}else{
							imageGraphics.drawImage(Textures.whitenormal2, (int)keyLength * i - (int)(keyLength / 2D), frameHeight - keyboardHeight, (int)keyLength + (int)keyLength, keyboardHeight, null);
						}
					}
				}
			}
			for(int i = 0; i < (largeKeyboard ? 256 : 128); i++){
				if(isWhiteKey[i] && isWhiteKey[i + 1]){
					if(keyStates[i].isPressed()){
						imageGraphics.drawImage(coloredKeyboardTexturesWhite.get(keyStates[i].pressedTracks().get(keyStates[i].pressedTracks().size() - 1)), (int)keyLength * i - (int)(keyLength / 2D), frameHeight - keyboardHeight, (int)keyLength + (int)(keyLength / 2D), keyboardHeight, null);
					}else{
						imageGraphics.drawImage(Textures.whitenormal, (int)keyLength * i - (int)(keyLength / 2D), frameHeight - keyboardHeight, (int)keyLength + (int)(keyLength / 2D), keyboardHeight, null);
					}
				}
			}
			if(largeKeyboard) imageGraphics.drawImage(Textures.whitenormal, (int)keyLength * 256 - (int)(keyLength / 2D), frameHeight - keyboardHeight, (int)keyLength + (int)(keyLength / 2D), keyboardHeight, null);
			for(int i = 0; i < (largeKeyboard ? 256 : 128); i++){
				if(!isWhiteKey[i]){
					if(keyStates[i].isPressed()){
						imageGraphics.drawImage(coloredKeyboardTexturesBlack.get(keyStates[i].pressedTracks().get(keyStates[i].pressedTracks().size() - 1)), (int)keyLength * i, frameHeight - keyboardHeight, (int)keyLength, blackKeyHeight, null);
					}else{
						imageGraphics.drawImage(Textures.blacknormal, (int)keyLength * i, frameHeight - keyboardHeight, (int)keyLength, blackKeyHeight, null);
					}
				}
			}
		}else{
			imageGraphics.drawImage(Textures.keys, 0, frameHeight - keyboardHeight, frameWidth, keyboardHeight, null);
		}
		imageGraphics.setColor(new Color(240, 240, 240));
		imageGraphics.drawLine(0, 0, frameWidth, 0);
		windowGraphics = panel.getGraphics();
		if(windowGraphics == null){
			return;
		}
		windowGraphics.drawImage(image, 0, 0, (TheGhastMidiPlayerMain.smaller ? 960 : 1280), (TheGhastMidiPlayerMain.smaller ? 540 : 720), panel);
		windowGraphics.dispose();
	}
	
	private class RenderThread implements Runnable {
		
		private List<Slice> slicesa;
		private int  nStart;
		private BufferedImage partImage;
		private Graphics2D gr;
		private int sentNotesC = 0;
		private int coreNum = 0;
		
		private RenderThread(int frameHeight, int coreNum){
			this.nStart = (int)((double)coreNum * ((largeKeyboard ? 256D : 128D) / (double)cores));
			this.slicesa = null;
			partImage = new BufferedImage(frameWidth / cores + 1, frameHeight, BufferedImage.TYPE_INT_RGB);
			gr = (Graphics2D) partImage.getGraphics();
			this.coreNum = coreNum;
		}
		
		private void reset(){
			gr.setColor(Color.BLACK);
			gr.fillRect(0, 0, partImage.getWidth(), partImage.getHeight());
			sentNotesC = 0;
		}
		
		private void setSlices(List<Slice> slicesa){
			this.slicesa = slicesa;
		}
		
		@Override
		public void run() {
			for(Slice s:slicesa){
				for(Note n:s.getNotesForCore(coreNum)) {
					updateKeyStatesWithNote(n, voiceLimit / cores);
					renderNote(n, gr, nStart);
				}
			}
		}
		
		private int endOffset;
		private int offset;
		
		private void renderNote(Note n, Graphics2D graphics, int keyOffset){
			if(!(n.getEnd() < tickPosition && n.getStart() > tickPosition + frameHeight)){
				endOffset = (int)(tickPosition + frameHeight - n.getEnd());
				offset = (int)(tickPosition + frameHeight - n.getStart());
				if(endOffset < 0){
					endOffset = 0;
				}
				if(offset >= 0 && offset - endOffset >= 0){
					if(offset > frameHeight){
						offset = frameHeight;
					}
					if(!SettingsDialog.fancyNotes){
						graphics.setColor(SettingsDialog.channelColoring ? trackColors.get(n.getChannel()) : trackColors.get(n.getTrack()));
						graphics.fillRect((int)(keyLength * (n.getPitch() - keyOffset)), endOffset, (int)keyLength, offset - endOffset);
						//graphics.setColor(Color.BLACK);
						Color col = SettingsDialog.channelColoring ? trackColors.get(n.getChannel()) : trackColors.get(n.getTrack());
						graphics.setColor(new Color(col.getRed() - 118 > 0 ? col.getRed() - 118 : 0, col.getGreen() - 118 > 0 ? col.getGreen() - 118 : 0, col.getBlue() - 118 > 0 ? col.getBlue() - 118 : 0));
						graphics.drawRect((int)(keyLength * (n.getPitch() - keyOffset)), endOffset, (int)keyLength, offset - endOffset);
					}else{
						int widthHere = (int)(keyLength * (double)((n.getPitch() - keyOffset) + 1) - keyLength * (double)(n.getPitch() - keyOffset));
						//g.setColor(channelColoring ? trackColors.get(n.getChannel()) : trackColors.get(n.getTrack()));
						//g.fillRect((int)(keyLength * (double)n.getPitch()), endOffset, (int)keyLength, offset - endOffset);
						Color col = SettingsDialog.channelColoring ? trackColors.get(n.getChannel()) : trackColors.get(n.getTrack());
						graphics.setColor(new Color(col.getRed() - 118 > 0 ? col.getRed() - 118 : 0, col.getGreen() - 118 > 0 ? col.getGreen() - 118 : 0, col.getBlue() - 118 > 0 ? col.getBlue() - 118 : 0));
						graphics.drawRect((int)(keyLength * (double)(n.getPitch() - keyOffset)), endOffset, widthHere - 1, offset - endOffset - 1);
						double gradientStepSize = 90D / (double)widthHere;
						for(int llll = 2; llll < widthHere; llll++){
							graphics.setColor(new Color((int)((double)col.getRed() - (90D - ((double)(llll - 1) * gradientStepSize))) > 0 ? (int)((double)col.getRed() - (90D - ((double)(llll - 1) * gradientStepSize))) : 0, (int)((double)col.getGreen() - (90D - ((double)(llll - 1) * gradientStepSize))) > 0 ? (int)((double)col.getGreen() - (90D - ((double)(llll - 1) * gradientStepSize))) : 0, (int)((double)col.getBlue() - (90D - ((double)(llll - 1) * gradientStepSize))) > 0 ? (int)((double)col.getBlue() - (90D - ((double)(llll - 1) * gradientStepSize))) : 0));
							graphics.drawLine((int)(keyLength * (double)(n.getPitch() - keyOffset) + widthHere - llll), endOffset + 1, (int)(keyLength * (double)(n.getPitch() - keyOffset) + widthHere - llll), endOffset + (offset - endOffset - 2));
						}
					}
				}
			}
		}
		
		private void updateKeyStatesWithNote(Note n, int limit){
			if(SettingsDialog.channelColoring){
				if(n.getStart() <= tickPosition + keyboardHeight && n.isOnPlayed() == false){
					n.setOnPlayed(true);
					keyStates[n.getPitch()].addPressedTrack(n.getChannel());
					keyStates[n.getPitch()].setIsPressed(true);
					if(sentNotesC <= limit && sound){
						if(n.getVelocity() > 5 || n.getVelocity() == 0){
							sendNoteToSynth(n.getPitch() - (largeKeyboard ? 60 : 0), n.getChannel(), n.getVelocity());
							notesPlaying[n.getChannel()][n.getPitch()]++;
							sentNotesC++;
						}
					}
				}
				if(n.getEnd() <= tickPosition + keyboardHeight && n.isOffPlayed() == false){
					n.setOffPlayed(true);
					keyStates[n.getPitch()].removePressedTrack(n.getChannel());
					if(keyStates[n.getPitch()].pressedTracks().isEmpty()){
						keyStates[n.getPitch()].setIsPressed(false);
					}
					if(sound && notesPlaying[n.getChannel()][n.getPitch()] > 0){
						sendNoteToSynth(n.getPitch() - (largeKeyboard ? 60 : 0), n.getChannel(), 0);
						notesPlaying[n.getChannel()][n.getPitch()]--;
					}
				}
			}else{
				if(n.getStart() <= tickPosition + keyboardHeight && n.isOnPlayed() == false){
					n.setOnPlayed(true);
					keyStates[n.getPitch()].addPressedTrack(n.getTrack());
					keyStates[n.getPitch()].setIsPressed(true);
					if(sentNotesC <= limit && sound){
						if(n.getVelocity() > 5 || n.getVelocity() == 0){
							sendNoteToSynth(n.getPitch() - (largeKeyboard ? 60 : 0), n.getChannel(), n.getVelocity());
							notesPlaying[n.getChannel()][n.getPitch()]++;
							sentNotesC++;
						}
					}
				}
				if(n.getEnd() <= tickPosition + keyboardHeight && n.isOffPlayed() == false && n.isOnPlayed()){
					n.setOffPlayed(true);
					keyStates[n.getPitch()].removePressedTrack(n.getTrack());
					if(keyStates[n.getPitch()].pressedTracks().isEmpty()){
						keyStates[n.getPitch()].setIsPressed(false);
					}
					if(sound && notesPlaying[n.getChannel()][n.getPitch()] > 0){
						sendNoteToSynth(n.getPitch() - (largeKeyboard ? 60 : 0), n.getChannel(), 0);
						notesPlaying[n.getChannel()][n.getPitch()]--;
					}
				}
			}
		}
		
	}
	
	private class MidiLoaderThread implements Runnable {
		
		private MidiLoaderThread(){
			
		}
		
		@Override
		public void run() {
			if(midiChooser.showOpenDialog(TheGhastMidiPlayerMain.frame) == JFileChooser.APPROVE_OPTION){
				if(!midiChooser.getSelectedFile().exists()){
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "The selected MIDI file doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				int loadednoets = 0;
				try {
					ProgressDialog progressDialog = ProgressDialog.showLoadDialog("Loading " + midiChooser.getSelectedFile().getName());
					fileMenu.setEnabled(false);
					helpMenu.setEnabled(false);
					preferencesMenu.setEnabled(false);
					playButton.setEnabled(false);
					MIDILoader ml = new MIDILoader(midiChooser.getSelectedFile());
					if(otherEvents != null){
						otherEvents.clear();
					}
					otherEvents = new ArrayList<MIDIEvent>();
					if(slices != null){
						slices.clear();
					}
					midiLength = ml.getLengthInTicks();
					slices = new ArrayList<Slice>();
					double numSlices = (double)midiLength / (double)frameHeight;
					numSlices = Math.ceil(numSlices);
					for(int i = 0; i < (int)numSlices; i++){
						slices.add(new Slice(frameHeight, i * frameHeight, cores));
					}
					System.out.println(numSlices);
					resolution = ml.getTPB();
					Track currentTrack = null;
					List<MIDIEvent> events = null;
					List<TempoEvent> tempos = new ArrayList<TempoEvent>();
					for(int i = 0; i < ml.getTrackCount(); i++){
						System.out.println("Preparing track " + Integer.toString(i) + " from " + Integer.toString(ml.getTrackCount()));
						ProgressDialog.status = "Preparing track " + Integer.toString(i + 1) + " from " + Integer.toString(ml.getTrackCount());
						ProgressDialog.progress = (int)((double)i / (double)ml.getTrackCount() * 50D) + 51;
						if(currentTrack != null){
							currentTrack.unload();
							currentTrack = null;
						}
						currentTrack = ml.getTracks().get(i);
						if(events != null){
							events.clear();
							events = null;
						}
						events = currentTrack.getEvents();
						InsertionSort.sortByTickTGMMIDIEvents(events);
						List<Note> tempNotes = new ArrayList<Note>();
						for(int o = 0; o < (largeKeyboard ? 256 : 128); o++){
							for(int l = 0; l < events.size(); l++){
								MIDIEvent event = events.get(l);
								if(event instanceof NoteOn){
									if(((NoteOn) event).getNoteValue() == o){
										tempNotes.add(new Note(event.getTick(), -1,o,i, ((NoteOn) event).getVelocity(), ((NoteOn) event).getChannel()));
									}
								}else if(event instanceof TempoEvent){
									boolean add = true;
									for(TempoEvent t:tempos){
										if(t.getTick() == event.getTick() && t.getBpm() == ((TempoEvent)event).getBpm()){
											add = false;
										}
									}
									if(add){
										tempos.add((TempoEvent)event);
									}
								}else if(event instanceof NoteOff){
									if(((NoteOff) event).getNoteValue() == o){
											if(!tempNotes.isEmpty()){
												int start = tempNotes.size();
												if(start != 0){
													start--;
												}
												for(int m = start; m > -1; m--){
													Note currentNote = tempNotes.get(m);
													if(!(currentNote.getEnd() >= 0) && currentNote.getStart() < event.getTick() && currentNote.getChannel() == ((NoteOff)event).getChannel()){
														currentNote.setEnd(event.getTick());
														break;
													}
												}
											}
									}
								}else if(event instanceof ProgramChangeEvent){
									boolean add = true;
									for(MIDIEvent ev:otherEvents){
										if(ev instanceof ProgramChangeEvent){
											if(((ProgramChangeEvent)ev).getChannel() == ((ProgramChangeEvent)event).getChannel() && ((ProgramChangeEvent)ev).getProgram() == ((ProgramChangeEvent)event).getProgram() && ((ProgramChangeEvent)ev).getTick() == ((ProgramChangeEvent)event).getTick()){
												add = false;
											}
										}
									}
									if(add) otherEvents.add(event);
								}
							}
							if(!tempNotes.isEmpty()){
								for(Note n:tempNotes){
									for(int oo = 0; oo < (int)numSlices; oo++){
										if(n.getStart() >= 0 && n.getEnd() >= 0 && n.getEnd() > n.getStart()){
											if(n.getStart() >= oo * frameHeight && n.getStart() < oo * frameHeight + frameHeight){
												slices.get(oo).addNote(n, (int)((double)n.getPitch() / ((largeKeyboard ? 256D : 128D) / (double)cores)));
												loadednoets++;
												break;
											}
											/*if(n.getEnd() >= slices.get(oo).getStartTick() && n.getStart() < slices.get(oo).getStartTick() + frameHeight){
													slices.get(oo).addNote(n, (int)((double)n.getPitch() / ((largeKeyboard ? 256D : 128D) / (double)cores)));
													if(!n.isOnPlayed()) {
														loadednoets++;
														n.setOnPlayed(true);
													}
											}*/
										}
									}
								}
								tempNotes.clear();
							}
						}
					}
					InsertionSort.sortByTickTGMTempos(tempos);
					otherEvents.addAll(tempos);
					noteCount = ml.getNoteCount();
					trackCount = ml.getTrackCount();
					progressDialog.close();
					fileMenu.setEnabled(true);
					helpMenu.setEnabled(true);
					preferencesMenu.setEnabled(true);
					playButton.setEnabled(true);
					System.gc();
					System.out.println("Done loading MIDI");
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Finished loading MIDI. Note count: " + Long.toString((long)noteCount) + " Loaded notes: " + Integer.toString(loadednoets), "Message", JOptionPane.INFORMATION_MESSAGE);
				} catch(Exception e){
					e.printStackTrace();
					JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error loading MIDI: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
					fileMenu.setEnabled(true);
					helpMenu.setEnabled(true);
					return;
				}
			}
		}
		
	}
	
	private void loadMidi(){
		if(midiLoaderThread != null){
			if(midiLoaderThread.isAlive()){
				return;
			}
		}
		midiLoaderThread = new Thread(new MidiLoaderThread());
		midiLoaderThread.start();
	}
	
	public String loadKernel(File file) throws Exception {
		FileInputStream str = new FileInputStream(file);
		String toReturn = "";
		while(str.available() > 0){
			toReturn = toReturn + (char)(byte)str.read();
		}
		str.close();
		return toReturn;
	}
	
	@Override
	public void windowOpened(WindowEvent e) {}
	
	@Override
	public void windowClosing(WindowEvent e) {
		running = false;
		try {
			if(TGMSynthesizer.isSynthStarted()){
				TGMSynthesizer.stopSynth();
			}
			if(fpsWindow != null && fpsWindow.isVisible()) fpsWindow.setVisible(false);
		}catch(Exception e2){
			e2.printStackTrace();
			JOptionPane.showMessageDialog(TheGhastMidiPlayerMain.frame, "Error stopping TGM's synthesizer: " + e2.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			System.exit(1);
		}
	}
	
	@Override
	public void windowClosed(WindowEvent e) {}
	
	@Override
	public void windowIconified(WindowEvent e) {}
	
	@Override
	public void windowDeiconified(WindowEvent e) {}
	
	@Override
	public void windowActivated(WindowEvent e) {}
	
	@Override
	public void windowDeactivated(WindowEvent e) {}
}