package theGhastModding.midiConverter.main;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.UIManager;
import javax.swing.filechooser.FileNameExtensionFilter;

import theGhastModding.midiConverter.midi.MIDIEvent;
import theGhastModding.midiConverter.midi.MIDILoader;
import theGhastModding.midiConverter.midi.Note;
import theGhastModding.midiConverter.midi.NoteOff;
import theGhastModding.midiConverter.midi.NoteOn;
import theGhastModding.midiConverter.midi.TempoEvent;
import theGhastModding.midiConverter.midi.Track;
import javax.swing.JCheckBox;

@SuppressWarnings("serial")
public class TheGhastMidiConverter extends JPanel implements Runnable {
	
	public static JFrame frame;
	private String selectedMidi = null;
	private JLabel lblMeminfo;
	private Thread t;
	private String status = "Ready";
	private JLabel lblStatus;
	private boolean converting = false;
	private JSlider slider;
	private JButton btnLoadMidi;
	private JButton btnExit;
	private JButton btnConvert;
	private JComboBox<String> comboBox;
	private JCheckBox chckbxUseSawtoothWave;
	
	public TheGhastMidiConverter(){
		super();
		setPreferredSize(new Dimension(600, 320));
		setLayout(null);
		
		JLabel lblLoadedMidi = new JLabel("");
		lblLoadedMidi.setBounds(10, 45, 580, 14);
		add(lblLoadedMidi);
		
		JFileChooser midiChooser = new JFileChooser();
		midiChooser.setFileFilter(new FileNameExtensionFilter("MIDI files", "midi", "MIDI", "mid", "MID"));
		btnLoadMidi = new JButton("Select MIDI");
		btnLoadMidi.setBounds(10, 11, 580, 23);
		add(btnLoadMidi);
		
		btnConvert = new JButton("Convert");
		btnConvert.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if(converting){
					JOptionPane.showMessageDialog(frame, "Can't convert: allready converting", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				convert();
			}
		});
		btnConvert.setBounds(10, 207, 580, 23);
		add(btnConvert);
		
		lblStatus = new JLabel("");
		lblStatus.setBounds(10, 266, 580, 14);
		add(lblStatus);
		
		lblMeminfo = new JLabel("");
		lblMeminfo.setBounds(10, 241, 580, 14);
		add(lblMeminfo);
		
		btnExit = new JButton("Exit");
		btnExit.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if(converting){
					JOptionPane.showMessageDialog(frame, "Can't exit: currently converting a MIDI", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				running = false;
				try {
					t.join();
				}catch(Exception e2){
					e2.printStackTrace();
				}
				System.exit(0);
			}
		});
		btnExit.setBounds(258, 286, 89, 23);
		add(btnExit);
		
		JLabel lblVolumedecreaseIf = new JLabel("Volume (decrease if output wav is just static or otherwise messed up):");
		lblVolumedecreaseIf.setBounds(10, 70, 580, 14);
		add(lblVolumedecreaseIf);
		
		slider = new JSlider();
		slider.setMaximum(2500);
		slider.setValue(1000);
		slider.setMinimum(100);
		slider.setBounds(10, 95, 580, 26);
		add(slider);
		
		lblWavFrequency = new JLabel("WAV frequency:");
		lblWavFrequency.setBounds(10, 157, 580, 14);
		add(lblWavFrequency);
		
		comboBox = new JComboBox<String>();
		comboBox.setModel(new DefaultComboBoxModel<String>(new String[] {"44100", "48000", "96000", "192000"}));
		comboBox.setSelectedIndex(1);
		comboBox.setBounds(10, 176, 130, 20);
		add(comboBox);
		
		JLabel lblpolyphonyFixedAt = new JLabel("(Polyphony fixed at 512)");
		lblpolyphonyFixedAt.setBounds(10, 132, 295, 14);
		add(lblpolyphonyFixedAt);
		
		chckbxUseSawtoothWave = new JCheckBox("Use sawtooth wave instead of square wave");
		chckbxUseSawtoothWave.setBounds(339, 128, 251, 23);
		add(chckbxUseSawtoothWave);
		btnLoadMidi.addActionListener(new ActionListener(){
			@Override
			public void actionPerformed(ActionEvent arg0) {
				if(converting){
					JOptionPane.showMessageDialog(frame, "Can't select MIDI: currently converting a MIDI", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				if(midiChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION){
					if(!midiChooser.getSelectedFile().exists()){
						JOptionPane.showMessageDialog(frame, "The selected file doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
						return;
					}
					lblLoadedMidi.setText("Selected MIDI is " + midiChooser.getSelectedFile().getName());
					selectedMidi = midiChooser.getSelectedFile().getPath();
				}
			}
		});
		
		t = new Thread(this);
		t.start();
	}
	
	private void convert(){
		DaConverter converter = new DaConverter();
		converter.beginConversion();
	}
	
	private static JFileChooser exportLocationChooser = null;
	
	private class DaConverter implements Runnable {
		
		private Thread t;
		
		private DaConverter(){
			t = new Thread(this);
		}
		
		public void beginConversion(){
			t.start();
		}
		
		@Override
		public void run() {
			converting = true;
			slider.setEnabled(false);
			btnLoadMidi.setEnabled(false);
			btnExit.setEnabled(false);
			btnConvert.setEnabled(false);
			comboBox.setEnabled(false);
			File midiFile = new File(selectedMidi);
			if(!midiFile.exists()){
				JOptionPane.showMessageDialog(frame, "The selected MIDI file doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
				converting = false;
				slider.setEnabled(true);
				btnLoadMidi.setEnabled(true);
				btnExit.setEnabled(true);
				btnConvert.setEnabled(true);
				comboBox.setEnabled(true);
				return;
			}
			double volume = (double)slider.getValue() / 10000D;
			File outputFile = new File("If this file is here, something went wrong.wav");
			if(exportLocationChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION){
				if(!exportLocationChooser.getSelectedFile().exists()){
					JOptionPane.showMessageDialog(frame, "The selected directory doesn't exist", "Error", JOptionPane.ERROR_MESSAGE);
					converting = false;
					slider.setEnabled(true);
					btnLoadMidi.setEnabled(true);
					btnExit.setEnabled(true);
					btnConvert.setEnabled(true);
					comboBox.setEnabled(true);
					return;
				}
				if(!exportLocationChooser.getSelectedFile().isDirectory()){
					JOptionPane.showMessageDialog(frame, "The selected file isn't a directory", "Error", JOptionPane.ERROR_MESSAGE);
					converting = false;
					slider.setEnabled(true);
					btnLoadMidi.setEnabled(true);
					btnExit.setEnabled(true);
					btnConvert.setEnabled(true);
					comboBox.setEnabled(true);
					return;
				}
				outputFile = new File(exportLocationChooser.getSelectedFile().getPath() + "\\" + midiFile.getName().replaceAll(".mid", ".wav").replaceAll(".midi", ".wav"));
			}else{
				converting = false;
				slider.setEnabled(true);
				btnLoadMidi.setEnabled(true);
				btnExit.setEnabled(true);
				btnConvert.setEnabled(true);
				comboBox.setEnabled(true);
				return;
			}
			status = "Loading MIDI";
			List<List<MIDIEvent>> midiEventsa = null;
			int TPB = 10;
			long lengthInTicks = 1000;
			MIDILoader loader = null;
			try {
				loader = new MIDILoader(midiFile);
				if(loader.getNoteCount() == 0){
					converting = false;
					slider.setEnabled(true);
					btnLoadMidi.setEnabled(true);
					btnExit.setEnabled(true);
					btnConvert.setEnabled(true);
					comboBox.setEnabled(true);
					JOptionPane.showMessageDialog(frame, "Error loading MIDI: MIDI contains no notes", "Error", JOptionPane.ERROR_MESSAGE);
					return;
				}
				midiEventsa = new ArrayList<List<MIDIEvent>>();
				for(Track t:loader.getTracks()){
					midiEventsa.add(t.getEvents());
				}
				TPB = loader.getTPB();
				lengthInTicks = loader.getLengthInTicks();
			} catch(Exception e){
				JOptionPane.showMessageDialog(frame, "Error loading MIDI: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
				converting = false;
				slider.setEnabled(true);
				btnLoadMidi.setEnabled(true);
				btnExit.setEnabled(true);
				btnConvert.setEnabled(true);
				comboBox.setEnabled(true);
				e.printStackTrace();
				return;
			}
			List<List<MIDIEvent>> midiEvents = new ArrayList<List<MIDIEvent>>();
			for(int i = 0; i < loader.getTrackCount(); i++){
				midiEvents.add(new ArrayList<MIDIEvent>());
				System.out.println("Preparing track " + i + " from " + loader.getTrackCount());
				List<MIDIEvent> events = midiEventsa.get(i);
				events = InsertionSort.sortByTickTGMMIDIEvents(events);
				List<Note> tempNotes = new ArrayList<Note>();
				for(int o = 0; o < 256; 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()));
							}
						}
						if(event instanceof TempoEvent){
							midiEvents.get(i).add((TempoEvent)event);
						}
						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;
											}
										}
									}
							}
						}
						
					}
					if(!tempNotes.isEmpty()){
					for(Note n:tempNotes){
						if(n.getStart() >= 0 && n.getEnd() >= 0 && n.getEnd() > n.getStart()){
							midiEvents.get(i).add(n);
						}
					}
					tempNotes.clear();
					}
				}
			}
			try {
				System.out.println("Finding tempo");
				TempoEvent firstTempo = null;
				List<TempoEvent> tempos = new ArrayList<TempoEvent>();
				for(List<MIDIEvent> eventList:midiEvents){
					for(MIDIEvent event:eventList){
						if(event instanceof TempoEvent){
							if(event.getTick() == 0){
								firstTempo = (TempoEvent)event;
							}
							tempos.add((TempoEvent)event);
						}
					}
				}
				System.out.println("Sorting tempos");
				tempos = InsertionSort.sortByTickTGMTempos(tempos);
				System.out.println("Calculating MIDI length in milliseconds");
				long lengthInMilliseconds = 0;
				if(tempos.size() > 1){
					for(int i = 1; i < tempos.size(); i++){
						lengthInMilliseconds += (long) (getLengthInMicrosecondsFromTickLength(tempos.get(i).getTick() - tempos.get(i - 1).getTick(), TPB, tempos.get(i - 1).getBpm()) / 1000D);
					}
					lengthInMilliseconds += (long) (getLengthInMicrosecondsFromTickLength(lengthInTicks - tempos.get(tempos.size() - 1).getTick(), TPB, tempos.get(tempos.size() - 1).getBpm()) / 1000D);
				}else{
					lengthInMilliseconds = (long) (getLengthInMicrosecondsFromTickLength(lengthInTicks, TPB, firstTempo.getBpm()) / 1000D);
				}
				System.out.println("Preparing WAV");
				status = "Preparing WAV";
				AdvancedMidiConverter conv = new AdvancedMidiConverter((int) (1 + lengthInMilliseconds / 1000), outputFile, Integer.parseInt(comboBox.getSelectedItem().toString()), volume, 512, chckbxUseSawtoothWave.isSelected());
				status = "Starting conversion";
				int[] currentEvents = new int[midiEvents.size()];
				boolean converting = true;
				double tickPosition = 0D;
				double percent = 0D;
				double currentBpm = firstTempo.getBpm();
				long timer = System.currentTimeMillis();
				System.out.println("Creating playingNotes list");
				System.out.println("Starting conversion");
				while(converting){
					tickPosition++;
				    if(tickPosition >= lengthInTicks){
						converting = false;
					}
					int toRemove = -1;
					for(int i = 0; i < tempos.size(); i++){
						if(tempos.get(i).getTick() <= tickPosition){
							currentBpm = tempos.get(i).getBpm();
							if(toRemove == -1){
								toRemove = i;
							}
						}
					}
					if(toRemove >= 0){
						tempos.remove(toRemove);
					}
				    for(int i = 0; i < midiEvents.size(); i++){
					    if(currentEvents[i] >= midiEvents.get(i).size()){
					    	continue;
					    }
					    if(midiEvents.get(i).get(currentEvents[i]).getTick() <= tickPosition){
					    	if(midiEvents.get(i).get(currentEvents[i]) instanceof Note){
					    		conv.encodeNote(((Note)midiEvents.get(i).get(currentEvents[i])).getPitch() - 60, (int) (getNoteLengthInMicroseconds((Note)midiEvents.get(i).get(currentEvents[i]), TPB, currentBpm) / 1000), (int) (getLengthInMicrosecondsFromTickLength(midiEvents.get(i).get(currentEvents[i]).getTick(), TPB, currentBpm) / 1000D), ((Note)midiEvents.get(i).get(currentEvents[i])).getVelocity(), ((Note)midiEvents.get(i).get(currentEvents[i])).getChannel());
					    	}
					    	if(midiEvents.get(i).get(currentEvents[i]) instanceof TempoEvent){
					    		currentBpm = ((TempoEvent)midiEvents.get(i).get(currentEvents[i])).getBpm();
					    	}
					    	currentEvents[i]++;
						    if(currentEvents[i] >= midiEvents.get(i).size()){
						    	continue;
						    }
					    	while(midiEvents.get(i).get(currentEvents[i]).getTick() <= tickPosition){
						    	if(midiEvents.get(i).get(currentEvents[i]) instanceof Note){
						    		conv.encodeNote(((Note)midiEvents.get(i).get(currentEvents[i])).getPitch() - 60, (int) (getNoteLengthInMicroseconds((Note)midiEvents.get(i).get(currentEvents[i]), TPB, currentBpm) / 1000), (int) (getLengthInMicrosecondsFromTickLength(midiEvents.get(i).get(currentEvents[i]).getTick(), TPB, currentBpm) / 1000D), ((Note)midiEvents.get(i).get(currentEvents[i])).getVelocity(), ((Note)midiEvents.get(i).get(currentEvents[i])).getChannel());
						    	}
						    	if(midiEvents.get(i).get(currentEvents[i]) instanceof TempoEvent){
						    		currentBpm = ((TempoEvent)midiEvents.get(i).get(currentEvents[i])).getBpm();
						    	}
					    		currentEvents[i]++;
							    if(currentEvents[i] >= midiEvents.get(i).size()){
							    	break;
							    }
					    	}
					    }
				    }
				    if(System.currentTimeMillis() - timer >= 1000){
					    percent = tickPosition / (double)lengthInTicks * 100D;
					    status = "Converting: " + Double.toString(percent) + "%";
					    timer = System.currentTimeMillis();
				    }
				}
				conv.finish();
			}catch(Exception e){
				JOptionPane.showMessageDialog(frame, "Error converting MIDI: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
				converting = false;
				slider.setEnabled(true);
				btnLoadMidi.setEnabled(true);
				btnExit.setEnabled(true);
				btnConvert.setEnabled(true);
				comboBox.setEnabled(true);
				e.printStackTrace();
				return;
			}
			status = "Finished. Ready.";
			converting = false;
			slider.setEnabled(true);
			btnLoadMidi.setEnabled(true);
			btnExit.setEnabled(true);
			btnConvert.setEnabled(true);
			comboBox.setEnabled(true);
		}
		
		private double getNoteLengthInMicroseconds(Note n, int TPB, double BPM){
			double lengthInMicroseconds = 0;
			lengthInMicroseconds = (60000 / (BPM * TPB)) * (n.getEnd() - n.getStart()) * 1000;
			return lengthInMicroseconds;
		}
		
		private double getLengthInMicrosecondsFromTickLength(double tickLength, int TPB, double BPM){
			double lengthInMicroseconds = 0;
			lengthInMicroseconds = (60000 / (BPM * TPB)) * tickLength * 1000;
			return lengthInMicroseconds;
		}
		
	}
	
	public static void main(String[] args){
		try {
	        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		}catch(Exception e){
			e.printStackTrace();
		}
		exportLocationChooser = new JFileChooser();
		exportLocationChooser.setDialogTitle("Choose export location");
		exportLocationChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		frame = new JFrame("TheGhastMidiConverter (Not trying to copy KMC. Please don't kill me Keppü)");
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setResizable(false);
		frame.setContentPane(new TheGhastMidiConverter());
		frame.pack();
		frame.setVisible(true);
	}
	
	private Runtime runtime;
	private boolean running = false;
	private JLabel lblWavFrequency;
	
	public void run(){
		runtime = Runtime.getRuntime();
		running = true;
		while(running){
			long maxMemory = runtime.maxMemory();
			long allocatedMemory = runtime.totalMemory();
			long freeMemory = runtime.freeMemory();
			lblMeminfo.setText("Memory usage info: Currently using " + Long.toString((allocatedMemory - freeMemory) / 1024 / 1024) + "MB out of " + Long.toString(maxMemory / 1024 / 1024) + "MB (" + Long.toString(allocatedMemory / 1024 / 1024) + "MB allocated)");
			lblStatus.setText(status);
			try {
				Thread.sleep(1000);
			}catch(Exception e){
				
			}
		}
	}
}