package theGhastModding.midiPlayerGL.gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import javax.imageio.ImageIO;
import javax.swing.JOptionPane;

import org.joml.Vector2f;
import org.lwjgl.BufferUtils;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import jouvieje.bass.structures.HSOUNDFONT;
import theGhastModding.midiPlayerGL.main.InsertionSort;
import theGhastModding.midiPlayerGL.main.TGMPGLMain;
import theGhastModding.midiPlayerGL.midi.MIDIEvent;
import theGhastModding.midiPlayerGL.midi.MIDILoader;
import theGhastModding.midiPlayerGL.midi.Note;
import theGhastModding.midiPlayerGL.midi.NoteOff;
import theGhastModding.midiPlayerGL.midi.NoteOn;
import theGhastModding.midiPlayerGL.midi.ProgramChangeEvent;
import theGhastModding.midiPlayerGL.midi.TempoEvent;
import theGhastModding.midiPlayerGL.midi.Track;
import theGhastModding.midiPlayerGL.models.Loader;
import theGhastModding.midiPlayerGL.models.NoteModel;
import theGhastModding.midiPlayerGL.models.RawModel;
import theGhastModding.midiPlayerGL.renderer.NoteRenderer;
import theGhastModding.midiPlayerGL.renderer.TextMasterRenderer;
import theGhastModding.midiPlayerGL.shaders.NoteShader;
import theGhastModding.midiPlayerGL.text.FontSet;
import theGhastModding.midiPlayerGL.text.FontType;
import theGhastModding.midiPlayerGL.text.GUIText;
import theGhastModding.synthesizer.main.MidiEvent;
import theGhastModding.synthesizer.main.TGMSynthesizer;

public class MainWindow {
	
	private long window;
	private int screenHeight = 720;
	private File screenshotsFolder;
	
	private List<List<NoteModel>> allNotes = new ArrayList<List<NoteModel>>();
	public static boolean largeKeyboard = false;
	private Loader loader;
	private List<List<Note>> midiNotes;
	private List<MIDIEvent> otherEvents;
	private long tickPosition = 0;
	private int TPB;
	private double TPS;
	private long lengthInTicks;
	private HSOUNDFONT soundfont;
	private long timerThen;
	private long timerNow;
	public static int poly = 0;
	private int[] currentEventsOn;
	private int currentOtherEvent;
	public static List<Color> trackColors;
	public static boolean preloadAll = true;
	private String midiFileName;
	
	public MainWindow(long window, String midiFileName, int cores) {
		try {
			this.window = window;
			this.midiFileName = midiFileName;
			screenshotsFolder = new File("screenshots/");
			if(!screenshotsFolder.exists()){
				try {
					screenshotsFolder.mkdir();
				}catch(Exception e){
					JOptionPane.showMessageDialog(null, "Error creating screenshots folder: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
					e.printStackTrace();
				}
			}
		} catch(Exception e){
			JOptionPane.showMessageDialog(null, "Error creating MainGameLoop: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
			GLFW.glfwDestroyWindow(window);
			System.exit(1);
		}
		try {
			TGMSynthesizer.startSynth(44100, false);
			TGMSynthesizer.setRenderingLimit(95);
			TGMSynthesizer.setMaxVoices(1024);
			TGMSynthesizer.setUseFx(true);
			TGMSynthesizer.setOnlyReleaseOnOverlapingInstances(false);
			soundfont = TGMSynthesizer.loadFont("default.sf2");
		} catch(Exception e) {
			JOptionPane.showMessageDialog(null, "Error starting synthesizer: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
			GLFW.glfwDestroyWindow(window);
			System.exit(1);
		}
	}
	
	public void run() {
		GLFW.glfwMakeContextCurrent(window);
		GL.createCapabilities();
		GLFW.glfwSetInputMode(window, GLFW.GLFW_CURSOR, GLFW.GLFW_CURSOR_NORMAL);
		Dimension d = getWindowSize(window);
		System.out.println(d.toString());
		
		loader = new Loader();
		NoteShader shader = new NoteShader();
		NoteRenderer renderer = new NoteRenderer(shader);
		TextMasterRenderer textRenderer = new TextMasterRenderer(this.loader);
		
		loadMIDI(midiFileName);
		if(midiNotes == null){
			GLFW.glfwDestroyWindow(window);
			System.exit(1);
			return;
		}
		if(midiNotes.isEmpty()){
			GLFW.glfwDestroyWindow(window);
			System.exit(1);
			return;
		}
		
		FontSet arial = null;
		try {
			FontType plain = new FontType(loader.loadTextureFromFile("/arial.png"), "/arial.fnt", window);
			FontType italic = new FontType(loader.loadTextureFromFile("/arial_italic.png"), "/arial_italic.fnt", window);
			FontType bold = new FontType(loader.loadTextureFromFile("/arial_bold.png"), "/arial_bold.fnt", window);
			FontType italicBold = new FontType(loader.loadTextureFromFile("/arial_bold_italic.png"), "/arial_bold_italic.fnt", window);
			arial = new FontSet(plain, italic, bold, italicBold);
		}catch(Exception e) {
			JOptionPane.showMessageDialog(null, "Error loading fonts: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
			GLFW.glfwDestroyWindow(window);
			GL.destroy();
			System.exit(1);
		}
		GUIText buildInfoText = new GUIText("Build " + TGMPGLMain.VERSION, 1, arial, new Vector2f(0f, 0f), 1f, false);
		buildInfoText.setColor(1, 1, 1);
		buildInfoText.setStyle(GUIText.PLAIN);
		textRenderer.loadText(buildInfoText);
		GUIText fpsText = new GUIText("FPS: 0", 1, arial, new Vector2f(0f, -0.06f), 1f, false);
		fpsText.setColor(1, 1, 1);
		fpsText.setStyle(GUIText.PLAIN);
		textRenderer.loadText(fpsText);
		GUIText voicesText = new GUIText("Active voices: 0/1024", 1, arial, new Vector2f(0f, -0.11f), 1f, false);
		voicesText.setColor(1, 1, 1);
		voicesText.setStyle(GUIText.PLAIN);
		textRenderer.loadText(voicesText);
		GUIText controllsText = new GUIText("Controlls: F2 = Take Screenshot, ESC = Exit", 1, arial, new Vector2f(0f, 0f), 1f, true);
		controllsText.setColor(1, 1, 1);
		controllsText.setStyle(GUIText.BOLD);
		textRenderer.loadText(controllsText);
		
		preparePlayback();
		currentEventsOn = new int[midiNotes.size()];
		for(int i = 0; i < currentEventsOn.length; i++) currentEventsOn[i] = 0;
		currentOtherEvent = 0;
		
		double frameTime = 1000000000D / 64D;
		long frameTimer = System.nanoTime();
		int counter = 0;
		int lastCounter = 0;
		int voices = 0;
		int lastVoices = 0;
		long lastTime = System.currentTimeMillis();
		long oldTickPos = 0;
		try {
			while(!GLFW.glfwWindowShouldClose(window) && tickPosition <= lengthInTicks + (TPS * 3)){
				if(System.nanoTime() - frameTimer >= frameTime){
					frameTimer = System.nanoTime();
					
					oldTickPos = tickPosition;
					timerNow = System.nanoTime();
				    tickPosition += ((((double)timerNow - (double)timerThen) / 1000000000D) * TPS);
				    timerThen = timerNow;
				    float moveAmount = (float)(tickPosition - oldTickPos) / (float)screenHeight * 2f;
				    for(int i = 0; i < allNotes.size(); i++) {
					    for(NoteModel nm:allNotes.get(i)) {
					    	nm.increaseY(moveAmount);
					    }
					}
				    poly = 0;
				    
				    parseMidiEvents();
				    
					if(GLFW.glfwGetKey(window, GLFW.GLFW_KEY_ESCAPE) == GL11.GL_TRUE){
						GLFW.glfwSetWindowShouldClose(window, true);
					}
					if(GLFW.glfwGetKey(window, GLFW.GLFW_KEY_F2) == GL11.GL_TRUE){
						screenshot();
					}
					GLFW.glfwPollEvents();
					renderer.prepare();
					renderer.render(allNotes);
					renderer.end();
					textRenderer.render();
					GLFW.glfwSwapBuffers(window);
					counter++;
				}
				if(System.currentTimeMillis() - lastTime >= 1000){
					lastTime = System.currentTimeMillis();
					System.out.println("FPS: " + Integer.toString(counter));
					if(counter != lastCounter) {
						lastCounter = counter;
						textRenderer.removeText(fpsText, true);
						fpsText.setText("FPS: " + Integer.toString(counter));
						textRenderer.loadText(fpsText);
					}
					voices = TGMSynthesizer.getActiveVoices();
					if(voices != lastVoices) {
						lastVoices = voices;
						textRenderer.removeText(voicesText, true);
						voicesText.setText("Active voices: " + Integer.toString(voices) + "/" + Integer.toString(TGMSynthesizer.getMaxVoices()));
						textRenderer.loadText(voicesText);
					}
					screenHeight = (int)MainWindow.getWindowSize(window).getHeight();
					try {
						System.out.println("Currently used voices: " + Integer.toString(TGMSynthesizer.getActiveVoices()) + "/" + Integer.toString(TGMSynthesizer.getMaxVoices()));
						System.out.println("Current rendering time: " + Float.toString(TGMSynthesizer.getRenderingTime()) + "/" + Float.toString(TGMSynthesizer.getRenderingLimit()));
					}catch(Exception e) {
						e.printStackTrace();
					}
					int noets = 0;
					for(int i = 0; i < allNotes.size(); i++) {
						noets += allNotes.get(i).size();
					}
					System.out.println("Current ammount of notes on screen: " + Integer.toString(noets));
					if(tickPosition >= TPS * 10 && controllsText != null) {
						textRenderer.removeText(controllsText, true);
						controllsText = null;
					}
					counter = 0;
				}
			}
		} catch(Exception e) {
			JOptionPane.showMessageDialog(null, "Error during playback: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
			GLFW.glfwDestroyWindow(window);
			GLFW.glfwTerminate();
			GL.destroy();
			shader.cleanUp();
			try {
				TGMSynthesizer.unloadFont(soundfont);
				TGMSynthesizer.stopSynth();
			}catch(Exception e2) {
				e2.printStackTrace();
			}
			System.exit(1);
		}
		screenshot();
		GLFW.glfwDestroyWindow(window);
		GLFW.glfwTerminate();
		GL.destroy();
		shader.cleanUp();
		loader.cleanUp();
		textRenderer.cleanUp();
		try {
			TGMSynthesizer.unloadFont(soundfont);
			TGMSynthesizer.stopSynth();
		}catch(Exception e) {
			e.printStackTrace();
		}
		int oo = (5 * 7 & 0xFA) * (8 & 12);
		System.out.println(oo);
	}
	
	private final int[] noteIndices = {2,0,3,3,0,1};
	
	private void addNote(Note n, int track) {
		long screenTickPos = n.getStart() - tickPosition;
		float glPos = (float)screenTickPos / (float)screenHeight * 2f + 1.0f;
		if(preloadAll) {
			n.getModel().setY(glPos);
			allNotes.get(track).add(n.getModel());
		}else {
			float glLength = (float)(n.getEnd() - n.getStart()) / (float)screenHeight * 2f;
			float[] vertices = {
				0, 0,
				NoteModel.noteWidth, 0,
				0, glLength,
				NoteModel.noteWidth, glLength
			};
			RawModel model = loader.loadToVAO(vertices, noteIndices);
			NoteModel nm = new NoteModel(model, n.getPitch(), glPos, track, glLength, n);
			allNotes.get(track).add(nm);
		}
	}
	
	private void parseMidiEvents() throws Exception {
	    for(int i = 0; i < midiNotes.size(); i++){
		    if(currentEventsOn[i] >= midiNotes.get(i).size()){
		    	continue;
		    }
		    if(midiNotes.get(i).get(currentEventsOn[i]).getStart() <= tickPosition){
		    	addNote(midiNotes.get(i).get(currentEventsOn[i]), i);
		    	currentEventsOn[i]++;
			    if(currentEventsOn[i] >= midiNotes.get(i).size()){
			    	continue;
			    }
		    	while(midiNotes.get(i).get(currentEventsOn[i]).getStart() <= tickPosition){
		    		addNote(midiNotes.get(i).get(currentEventsOn[i]), i);
		    		currentEventsOn[i]++;
				    if(currentEventsOn[i] >= midiNotes.get(i).size()){
				    	break;
				    }
		    	}
		    }
	    }
	    if(currentOtherEvent < otherEvents.size()) {
		    if(otherEvents.get(currentOtherEvent).getTick() <= tickPosition) {
		    	processEvent(otherEvents.get(currentOtherEvent));
		    	currentOtherEvent++;
		    }
		    if(currentOtherEvent < otherEvents.size()) {
		    	while(currentOtherEvent < otherEvents.size() && otherEvents.get(currentOtherEvent).getTick() <= tickPosition) {
			    	processEvent(otherEvents.get(currentOtherEvent));
			    	currentOtherEvent++;
		    	}
		    }
	    }
	}
	
	private void processEvent(MIDIEvent midiEvent) throws Exception {
		if(midiEvent instanceof TempoEvent) {
			TPS = (((TempoEvent)midiEvent).getBpm() / 60) * TPB;
		}
		if(midiEvent instanceof ProgramChangeEvent){
			TGMSynthesizer.sendEvent(new MidiEvent(MidiEvent.MIDI_EVENT_PROGRAM, ((ProgramChangeEvent)midiEvent).getChannel(), ((ProgramChangeEvent)midiEvent).getProgram(), 0));
		}
	}
	
	private void preparePlayback() {
		TempoEvent firstTempo = null;
		for(MIDIEvent event:otherEvents){
			if(event instanceof TempoEvent){
				if(event.getTick() == 0){
					firstTempo = (TempoEvent)event;
				}
			}
		}
		if(firstTempo == null) {
			System.err.println("No tempo event found at tick 0; using default BPM of 120");
			firstTempo = new TempoEvent(0, 120, 0);
		}
		trackColors = new ArrayList<Color>();
		trackColors.add(new Color(51, 102, 255));
		trackColors.add(new Color(255, 126, 51));
		trackColors.add(new Color(51, 255, 102));
		trackColors.add(new Color(255, 51, 129));
		trackColors.add(new Color(51, 255, 255));
		trackColors.add(new Color(228, 51, 255));
		trackColors.add(new Color(153, 255, 51));
		trackColors.add(new Color(75, 51, 255));
		Random rnd = new Random();
		for(int i = 0; i < midiNotes.size(); i++) {
			Color c = null;
			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);
		}
		allNotes = new ArrayList<List<NoteModel>>();
		for(int i = 0; i < midiNotes.size(); i++) {
			for(Note n:midiNotes.get(i)) {
				n.setOnPlayed(false);
				n.setOffPlayed(false);
			}
			allNotes.add(new ArrayList<NoteModel>());
		}
		TPS = (firstTempo.getBpm() / 60) * TPB;
		tickPosition = 0;
		timerThen = System.nanoTime();
	}
	
	public void screenshot(){
		try {
			Dimension windowSize = MainWindow.getWindowSize(window);
			int width = (int)windowSize.width;
			int height = (int)windowSize.height;
			GL11.glReadBuffer(GL11.GL_FRONT);
			ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4);
			GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);
			File file = new File(screenshotsFolder.getPath() + "/" + ZonedDateTime.now().toString().replaceAll(":", "_").replaceAll("/", "_") + TGMPGLMain.NAME.replaceAll(" ", "_").replaceAll(",", "") + "_" + TGMPGLMain.VERSION + ".png");
			BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
			for(int x = 0; x < width; x++) {
			    for(int y = 0; y < height; y++) {
			        int i = (x + (width * y)) * 4;
			        int r = buffer.get(i) & 0xFF;
			        int g = buffer.get(i + 1) & 0xFF;
			        int b = buffer.get(i + 2) & 0xFF;
			        image.setRGB(x, height - (y + 1), (0xFF << 24) | (r << 16) | (g << 8) | b);
			    }
			}
			ImageIO.write(image, "png", file);
		}catch(Exception e){
			JOptionPane.showMessageDialog(null, "Error taking screenshot: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
			return;
		}
	}
	
	public static Dimension getWindowSize(long window){
		IntBuffer w = BufferUtils.createIntBuffer(1);
		IntBuffer h = BufferUtils.createIntBuffer(1);
		GLFW.glfwGetWindowSize(window, w, h);
		return new Dimension(w.get(0), h.get(0));
	}
	
	private void loadMIDI(String filename) {
		try {
			MIDILoader loader = new MIDILoader(new File(filename));
			if(loader.getNoteCount() == 0){
				JOptionPane.showMessageDialog(null, "Error loading MIDI", "Error", JOptionPane.ERROR_MESSAGE);
				return;
			}
			midiNotes = new ArrayList<List<Note>>();
			otherEvents = new ArrayList<MIDIEvent>();
			Track currentTrack  = null;
			List<MIDIEvent> events = null;
			long noteCount = 0;
			for(int i = 0; i < loader.getTrackCount(); i++){
				System.out.println("Preparing track " + Integer.toString(i + 1) + " from " + Integer.toString(loader.getTrackCount()));
				if(currentTrack != null){
					currentTrack.unload();
					currentTrack = null;
					System.gc();
				}
				currentTrack = loader.getTracks().get(i);
				if(events != null){
					events.clear();
					events = null;
				}
				events = currentTrack.getEvents();
				midiNotes.add(new ArrayList<Note>());
				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,((NoteOn) event).getNoteValue(),i, ((NoteOn) event).getVelocity(), ((NoteOn) event).getChannel()));
							//}
						}else if(event instanceof TempoEvent){
							boolean add = true;
							for(MIDIEvent t:otherEvents){
								if(t instanceof TempoEvent) {
									if(t.getTick() == event.getTick() && ((TempoEvent)t).getBpm() == ((TempoEvent)event).getBpm()){
										add = false;
									}
								}
							}
							if(add){
								otherEvents.add((TempoEvent)event);
							}
						}else if(event instanceof NoteOff){
							if(!tempNotes.isEmpty()){
								int start = tempNotes.size() - 1;
								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.getPitch() == ((NoteOff)event).getNoteValue()){
										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) {
							if(n.getStart() >= 0 && n.getEnd() >= 0 && n.getEnd() > n.getStart()){
								midiNotes.get(i).add(n);
								if(preloadAll) {
									float glLength = (float)(n.getEnd() - n.getStart()) / (float)screenHeight * 2f;
									float[] vertices = {
											0, 0,
											NoteModel.noteWidth, 0,
											0, glLength,
											NoteModel.noteWidth, glLength
									};
									RawModel model = this.loader.loadToVAO(vertices, noteIndices);
									NoteModel nm = new NoteModel(model, n.getPitch(), 0, i, glLength, n);
									midiNotes.get(i).get(midiNotes.get(i).size() - 1).setModel(nm);
								}
								noteCount++;
							}
						}
						tempNotes.clear();
					}
				//}
			}
			for(int i = 0; i < midiNotes.size(); i++) {
				System.out.println("Sorting track " + Integer.toString(i + 1) + " from " + Integer.toString(midiNotes.size()));
				midiNotes.set(i, InsertionSort.sortByTickTGMNotes(midiNotes.get(i)));
			}
			otherEvents = InsertionSort.sortByTickTGMMIDIEvents(otherEvents);
			TPB = loader.getTPB();
			lengthInTicks = loader.getLengthInTicks();
			System.gc();
			System.out.println("Done loading MIDI");
			System.out.println("Note count: " + Long.toString(noteCount) + "/" + Integer.toString(loader.getNoteCount()));
			loader.unload();
			System.gc();
		} catch(Exception e){
			e.printStackTrace();
			JOptionPane.showMessageDialog(null, "Error loading MIDI", "Error", JOptionPane.ERROR_MESSAGE);
			midiNotes = null;
			otherEvents = null;
			return;
		}
	}
	
}