package com.stereodustparticles.mooler_caster_console;

// SDP Mooler Caster Console
// Ridiculously simple radio DJ program, with a 16-slot soundboard and 2-deck music player
// Written by Ben A. (IfYouLikeGoodIdeas)

// This class provides the song playback deck functionality

import java.awt.Color;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;

import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JButton;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.UIManager;

import org.kc7bfi.jflac.metadata.Metadata;
import org.kc7bfi.jflac.metadata.StreamInfo;
import org.kc7bfi.jflac.metadata.VorbisComment;
import org.kc7bfi.jflac.sound.spi.Flac2PcmAudioInputStream;

import javazoom.spi.mpeg.sampled.convert.DecodedMpegAudioInputStream;
import javazoom.spi.vorbis.sampled.convert.DecodedVorbisAudioInputStream;
import net.sourceforge.jaad.mp4.api.MetaData;
import net.sourceforge.jaad.spi.javasound.MP4AudioInputStream;

public class Deck {
	// Instance variables
	AudioInputStream audio = null;
	SourceDataLine line = null;
	DeckThread playerThread = null;
	byte[] playBuffer = new byte[4096];
	int duration = 0; // in tenths of seconds
	int remain = 0; // also in tenths of seconds
	boolean wasLogged = false;
	
	// Time to start flashing the timer
	static int flashPoint = SDPConsole.prefs.getInt(DeckPrefs.FLASH_POINT, DeckPrefs.DEFAULT_FLASH_POINT) * 10;
	static boolean shouldFlash = SDPConsole.prefs.getBoolean(DeckPrefs.FLASH_TIMERS, DeckPrefs.DEFAULT_FLASH_TIMERS);
	
	// Playlist logging
	static boolean shouldLog = SDPConsole.prefs.getBoolean(DeckPrefs.LOG_ENABLED, DeckPrefs.DEFAULT_LOG_ENABLED);
	static File logDirectory = new File(SDPConsole.prefs.get(DeckPrefs.LOG_DIR, DeckPrefs.DEFAULT_LOG_DIR));
	static String logFileName = SDPConsole.makeNewLogFileName();
	
	// Constants
	final static Color DEFAULT_PROGRESS_BAR_COLOR = UIManager.getColor("ProgressBar.foreground");
	
	// GUI fields
	DeckFields fields = null;
	
	// Load a file into this Deck
	public void load(File file) throws UnsupportedAudioFileException, IOException, LineUnavailableException, IllegalArgumentException {
		// If there was a previously loaded file, stop it if necessary, then close its stream and line
		if ( playerThread != null && playerThread.isAlive() ) {
			try {
				playerThread.stopAudio();
			}
			catch (InterruptedException e) {
				// This shouldn't happen, but Java makes me put it here anyway
				System.err.println("So this happened:");
				e.printStackTrace();
			}
		}
		if ( line != null ) {
			line.close();
		}
		if ( audio != null ) {
			audio.close();
			System.gc(); // For safe measure
		}
		
		// Load the file and set up decoding
		// (From http://www.javalobby.org/java/forums/t18465.html)
		AudioInputStream in = AudioSystem.getAudioInputStream(file.toURI().toURL());
		
		AudioFormat baseFormat = in.getFormat();
		AudioFormat decodedFormat = new AudioFormat(
			AudioFormat.Encoding.PCM_SIGNED, // Encoding to use
			baseFormat.getSampleRate(),	  // sample rate (same as base format)
			16,				  // sample size in bits (thx to Javazoom)
			baseFormat.getChannels(),	  // # of Channels
			baseFormat.getChannels()*2,	  // Frame Size
			baseFormat.getSampleRate(),	  // Frame Rate
			false				  // Is Big Endian? (No)
		);
		audio = AudioSystem.getAudioInputStream(decodedFormat, in);
		
		// Get the output line (using the method below)
		line = getLine(decodedFormat);
		
		// Update metadata fields
		if ( fields != null ) {
			// Determine file format and act accordingly
			if ( audio instanceof Flac2PcmAudioInputStream ) {
				// File is FLAC
				Flac2PcmAudioInputStream flacAudio = (Flac2PcmAudioInputStream)audio;
				
				if ( flacAudio.available() > 0 ) { // This line is really only here to force the decoder to initialize...
					// Keep track of whether the comment header was found
					boolean foundComment = false;
					
					// Loop across the metadata fields
					for ( Metadata meta : flacAudio.getMetaData() ) {
						// If this metadata field is a Vorbis Comment... (FLAC uses those for its metadata)
						if ( meta instanceof VorbisComment ) {
							foundComment = true;
							VorbisComment comment = (VorbisComment)meta;
							
							// Get title and artist, or use generic data if an exception is thrown (i.e. that field is probably not present)
							try {
								fields.artist.setText(comment.getCommentByName("ARTIST")[0]);
							}
							catch (Exception e) {
								fields.artist.setText("(Unknown Artist)");
							}
							
							try {
								fields.title.setText(comment.getCommentByName("TITLE")[0]);
							}
							catch (Exception e) {
								fields.title.setText(file.getName());
							}
							
							// End the loop here, we don't need to look any further
							break;
						}
					}
					
					// If no comment header was ever found, use generic data
					if (! foundComment) {
						fields.artist.setText("(Unknown Artist)");
						fields.title.setText(file.getName());
					}
					
					// Pull the StreamInfo field and calculate duration from its data
					StreamInfo info = flacAudio.getStreamInfo();
					duration = (int)(((double)info.getTotalSamples() / (double)info.getSampleRate()) * 10);
				}
			}
			else if ( in instanceof MP4AudioInputStream ) {
				// File is M4A (AAC)
				MetaData meta = ((MP4AudioInputStream) in).getMetaData();
				
				// Extract the fields we need
				String title = meta.get(MetaData.Field.TITLE);
				String artist = meta.get(MetaData.Field.ARTIST);
				duration = (int)(((MP4AudioInputStream) in).getDuration() * 10.0);
				
				// Did we get usable data?  If so, display it.  If not, use generic stuff.
				if ( title != null && title != "" ) {
					fields.title.setText(title);
				}
				else {
					fields.title.setText(file.getName());
				}
				
				if ( artist != null && artist != "" ) {
					fields.artist.setText(artist);
				}
				else {
					fields.artist.setText("(Unknown Artist)");
				}
			}
			else {
				// File is some other format; assume it supports the standard Java Sound metadata fields
				// If we still don't get good data this way, fill the fields with generic data
				AudioFileFormat fileFormat = AudioSystem.getAudioFileFormat(file);
				
				// Pull the Properties structure and put it in a convenient place
				Map<String, Object> properties = fileFormat.properties();
				
				// If we actually got anything...
				if ( properties != null ) {
					// Get artist
					// Use generic data if nothing gets returned
					Object artist = properties.get("author");
					if ( artist instanceof String && ! ((String)artist).equals("") ) {
						fields.artist.setText( (String)artist );
					}
					else {
						fields.artist.setText("(Unknown Artist)");
					}
					
					// Get title
					// Use generic data (filename) if nothing gets returned
					Object title = properties.get("title");
					if ( title instanceof String && ! ((String)title).equals("") ) {
						fields.title.setText( (String)title );
					}
					else {
						fields.title.setText(file.getName());
					}
					
					// Get duration - once again, different methods for different formats
					if ( audio instanceof DecodedMpegAudioInputStream || audio instanceof DecodedVorbisAudioInputStream ) {
						// MP3 or OGG - In this case, duration is provided in metadata, because the numbers for the calculation aren't necessarily known
						duration = (int)( (long)properties.get("duration") / 100000 );
					}
					else {
						// Other, presumably WAV - In this case, we have to do the calculation ourselves, but at least we can get the numbers easily
						// http://stackoverflow.com/questions/3009908/how-do-i-get-a-sound-files-total-time-in-java
						long frames = audio.getFrameLength();
						double durationInSeconds = (frames+0.0) / decodedFormat.getFrameRate();
						duration = (int)(durationInSeconds * 10);
					}
				}
				// If we got null for the properties table, use generic data across the board
				else {
					fields.title.setText(file.getName());
					fields.artist.setText("(Unknown Artist)");
					duration = 0;
				}
			}
			// Set counter and progress bar
			remain = duration;
			updateTime(duration);
			fields.progress.setMaximum(duration);
		}
		
		// Newly-loaded file has not been logged yet
		wasLogged = false;
	}
	
	// Start/stop playback
	public void playButton() {
		// If there's actually a file loaded...
		if ( line != null && audio != null ) {
			// If the player thread isn't running (or doesn't exist in the first place)
			if ( playerThread == null || ! playerThread.isAlive() ) {
				// Create the player thread and start it
				playerThread = new DeckThread(this);
				playerThread.start();
			}
			else {
				// Tell the player thread to stop playback
				try {
					playerThread.stopAudio();
				}
				catch (InterruptedException e) {
					// This shouldn't happen, but Java makes me plan for it anyway
					System.err.println("Microwave time!");
					e.printStackTrace();
				}
			}
			
			// Check if log needs to be updated and do so if necessary
			if ( shouldLog && ! wasLogged ) {
				// Based on https://www.mkyong.com/java/how-to-append-content-to-file-in-java/
				BufferedWriter bw = null;
				FileWriter fw = null;

				try {
					File file = new File(logDirectory, logFileName);

					// Create the file if it doesn't exist yet
					if ( ! file.exists() ) {
						file.createNewFile();
					}

					fw = new FileWriter(file.getAbsoluteFile(), true);
					bw = new BufferedWriter(fw);

					bw.write(fields.artist.getText() + " - " + fields.title.getText() + SDPConsole.SYSTEM_LINE_END);
					
					wasLogged = true;
				}
				catch (IOException e) {
					JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "Could not write to the playout log file: " + e.getMessage() + "\n\nCheck your settings, then try again.  If the problem persists, throw your computer into the pool.", "Error Writing Log File", JOptionPane.ERROR_MESSAGE);					
					e.printStackTrace();
				}
				finally {
					try {
						if (bw != null) {
							bw.close();
						}
						if (fw != null) {
							fw.close();
						}
					}
					catch (IOException e) {
						System.err.println("I'm having really bad luck today - I couldn't even close the log stream!");
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	// From http://www.javazoom.net/mp3spi/documents.html: Get a line for a given format
	private SourceDataLine getLine(AudioFormat audioFormat) throws LineUnavailableException {
	  SourceDataLine res = null;
	  DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
	  res = (SourceDataLine) AudioSystem.getLine(info);
	  res.open(audioFormat);
	  return res;
	}
	
	// Associate the given GUI fields with this Deck
	public void associateFields(JButton playButton, JTextField title, JTextField artist, JTextField time, JProgressBar progress) {
		fields = new DeckFields();
		fields.playButton = playButton;
		fields.artist = artist;
		fields.title = title;
		fields.time = time;
		fields.progress = progress;
	}
	
	// Update the GUI time field and progress bar (parameter is remaining time, in tenths of seconds)
	void updateTime(int time) {
		// Prevent negative durations (so failure to parse metadata doesn't make things quite as wonky...)
		if (time < 0) time *= -1;
		
		// Set counter and progress bar
		int tenths = time % 10;
		int sec = (time / 10) % 60;
		int min = (time / 10) / 60;
		fields.time.setText(min + ":" + String.format("%02d", sec) + "." + tenths);
		fields.progress.setValue(duration - time);
		
		// Flashing timer
		if ( shouldFlash && time < flashPoint && time > 0 ) {
			if ( tenths >= 5 ) {
				fields.time.setForeground(Color.RED);
				fields.progress.setForeground(Color.RED);
			}
			else {
				fields.time.setForeground(Color.BLACK);
				fields.progress.setForeground(DEFAULT_PROGRESS_BAR_COLOR);
			}
		}
	}
}
