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)

// Uses MP3SPI, VorbisSPI, jFLAC, and JAADec to provide MP3, Vorbis, FLAC, and AAC support, respectively

// NOTE: jFLAC 1.3's jar file won't work out of the box, as it doesn't contain the proper SPI metadata.
// Use the provided "spi-fix" jar instead.

// NOTE: You must use tritonus_share version 0.3.7-2 (or higher, perhaps) for Vorbis decoding to work!

// NOTE: You must have the special SDP version (included) of JAADec for AAC decoding (see README)

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;

import java.awt.Font;
import java.awt.Color;
import javax.swing.JTextField;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.JProgressBar;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.prefs.Preferences;
import java.awt.event.ActionEvent;
import javax.swing.JSeparator;
import javax.swing.JMenuBar;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import java.awt.event.KeyEvent;
import java.awt.event.InputEvent;

public class SDPConsole {
	
	// Program version number
	public static final double PROG_VERSION = 1.06; // v1.06a, 3/23/17
	
	// Soundboard Layout File Filter
	public static final FileNameExtensionFilter LAYOUT_FILE_FILTER = new FileNameExtensionFilter("Soundboard Layouts (*.bla)", "bla");
	
	// State variables
	static File lastSpotPath = null;
	static File lastDeckPath = null;
	static File lastLayoutPath = null;
	
	// Layout file
	static File currentLayout = null;
	static boolean fileUpToDate = true;
	
	// Preferences table
	public static Preferences prefs = Preferences.userRoot().node("com.stereodustparticles.mooler_caster_console");
	
	// GUI stuff (most generated by WindowBuilder)
	private static JFrame SDPMain;
	private static JTextField deck1Artist;
	private static JTextField deck1Title;
	private static JTextField deck2Title;
	private static JTextField deck2Artist;
	private static JTextField deck1Time;
	private static JTextField deck2Time;
	private static Spot[] spots = new Spot[16];
	private static Deck[] decks = {new Deck(), new Deck()};
	
	// Misc. constants
	public static final String SYSTEM_LINE_END = System.getProperty("line.separator");

	/**
	 * "Main" method - Launch the application.
	 */
	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				try {
					initialize();
					SDPConsole.SDPMain.setVisible(true);
					
					// Check for a startup layout and load it if present
					if ( prefs.getBoolean(SpotPrefs.STARTUP_LAYOUT, SpotPrefs.DEFAULT_STARTUP_LAYOUT) ) {
						File startLayout = new File(prefs.get(SpotPrefs.STARTUP_LAYOUT_LOC, SpotPrefs.DEFAULT_STARTUP_LAYOUT_LOC));
						
						new Thread(new Runnable() {
							public void run() {
								BoardLayout.loadFromFile(startLayout, spots);
							}
						}).start();
						
						// Save the startup layout's folder
						lastLayoutPath = startLayout.getParentFile();
					}
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
	}

	/**
	 * Initialize the contents of the frame.
	 * Most of this was generated by Eclipse WindowBuilder, so it probably won't be terribly readable...
	 * The good stuff is below all of this...
	 */
	private static void initialize() {
		SDPMain = new JFrame();
		SDPMain.setResizable(false);
		SDPMain.setTitle("SDP Mooler Caster Console");
		SDPMain.setBounds(100, 100, 411, 685);
		SDPMain.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		SDPMain.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e) {
				// When closing the main window, run the "Save changes" method, and don't close if Cancel was pressed
				if ( ! confirmClear() ) return;
				System.exit(0);
			}
		});
		SDPMain.getContentPane().setLayout(null);
		
		// Generate spot buttons for soundboard
		// Note that these buttons don't show up in the designer
		for ( int i = 0; i < 16; i++ ) {
			spots[i] = new Spot(i);
			SDPMain.getLayeredPane().add(spots[i].getButton(), 0, 0);
			SDPMain.getLayeredPane().add(spots[i].getCounter(), 1, 0);
		}
		
		// Deck 1 Label
		JLabel lblDeck1 = new JLabel("Deck 1:"); // Instantiate and set text
		lblDeck1.setBounds(10, 418, 70, 14); // Set position and size
		SDPMain.getContentPane().add(lblDeck1); // Add it to the window
		
		// Deck 2 Label
		JLabel lblDeck2 = new JLabel("Deck 2:");
		lblDeck2.setBounds(10, 523, 70, 14);
		SDPMain.getContentPane().add(lblDeck2);
		
		// Deck 1 Play Button
		JButton deck1Play = new JButton("PLAY");
		deck1Play.addActionListener(new ActionListener() { // Add an event handler
			// Method to execute when the button is clicked
			public void actionPerformed(ActionEvent e) {
				decks[0].playButton();
			}
		});
		deck1Play.setBackground(Color.GREEN);
		deck1Play.setFont(new Font("Tahoma", Font.BOLD, 18));
		deck1Play.setBounds(10, 437, 89, 72);
		SDPMain.getContentPane().add(deck1Play);
		
		// Deck 2 Play Button
		JButton deck2Play = new JButton("PLAY");
		deck2Play.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				decks[1].playButton();
			}
		});
		deck2Play.setBackground(Color.GREEN);
		deck2Play.setFont(new Font("Tahoma", Font.BOLD, 18));
		deck2Play.setBounds(10, 541, 89, 72);
		SDPMain.getContentPane().add(deck2Play);
		
		// Deck 1 Load Button
		JButton deck1Load = new JButton("LOAD");
		deck1Load.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				loadDeck(0);
			}
		});
		deck1Load.setBackground(Color.ORANGE);
		deck1Load.setBounds(109, 486, 74, 23);
		SDPMain.getContentPane().add(deck1Load);
		
		// Deck 2 Load Button
		JButton deck2Load = new JButton("LOAD");
		deck2Load.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent arg0) {
				loadDeck(1);
			}
		});
		deck2Load.setBackground(Color.ORANGE);
		deck2Load.setBounds(109, 590, 74, 23);
		SDPMain.getContentPane().add(deck2Load);
		
		// Deck 1 Artist Box
		deck1Artist = new JTextField();
		deck1Artist.setEditable(false);
		deck1Artist.setText("No track loaded.");
		deck1Artist.setBounds(109, 437, 229, 20);
		SDPMain.getContentPane().add(deck1Artist);
		deck1Artist.setColumns(10);
		
		// Deck 1 Title Box
		deck1Title = new JTextField();
		deck1Title.setText("No track loaded.");
		deck1Title.setEditable(false);
		deck1Title.setColumns(10);
		deck1Title.setBounds(109, 462, 286, 20);
		SDPMain.getContentPane().add(deck1Title);
		
		// Deck 2 Title Box
		deck2Title = new JTextField();
		deck2Title.setText("No track loaded.");
		deck2Title.setEditable(false);
		deck2Title.setColumns(10);
		deck2Title.setBounds(109, 566, 286, 20);
		SDPMain.getContentPane().add(deck2Title);
		
		// Deck 2 Artist Box
		deck2Artist = new JTextField();
		deck2Artist.setText("No track loaded.");
		deck2Artist.setEditable(false);
		deck2Artist.setColumns(10);
		deck2Artist.setBounds(109, 541, 229, 20);
		SDPMain.getContentPane().add(deck2Artist);
		
		// Deck 1 Time Box
		deck1Time = new JTextField();
		deck1Time.setEditable(false);
		deck1Time.setText("0:00.0");
		deck1Time.setBounds(345, 437, 50, 20);
		SDPMain.getContentPane().add(deck1Time);
		deck1Time.setColumns(10);
		
		// Deck 2 Time Box
		deck2Time = new JTextField();
		deck2Time.setText("0:00.0");
		deck2Time.setEditable(false);
		deck2Time.setColumns(10);
		deck2Time.setBounds(345, 541, 50, 20);
		SDPMain.getContentPane().add(deck2Time);
		
		// Deck 1 Progress Bar
		JProgressBar deck1Progress = new JProgressBar();
		deck1Progress.setBounds(193, 486, 202, 23);
		SDPMain.getContentPane().add(deck1Progress);
		
		// Deck 2 Progress Bar
		JProgressBar deck2Progress = new JProgressBar();
		deck2Progress.setBounds(193, 590, 202, 23);
		SDPMain.getContentPane().add(deck2Progress);
		
		// Separator Bar between Spots and Decks
		JSeparator separator = new JSeparator();
		separator.setBounds(10, 410, 385, 2);
		SDPMain.getContentPane().add(separator);
		
		// Associate deck controls/fields with their corresponding Deck objects
		decks[0].associateFields(deck1Play, deck1Title, deck1Artist, deck1Time, deck1Progress);
		decks[1].associateFields(deck2Play, deck2Title, deck2Artist, deck2Time, deck2Progress);
		
		// The Menu Bar
		JMenuBar menuBar = new JMenuBar();
		SDPMain.setJMenuBar(menuBar);
		
		// - File Menu
		JMenu mnFile = new JMenu("File");
		menuBar.add(mnFile);
		
		// - - New Layout
		JMenuItem mntmNew = new JMenuItem("New Soundboard Layout");
		mntmNew.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_MASK));
		mntmNew.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// Confirm clearing of the board, return here if Cancel is pressed
				if ( ! confirmClear() ) return;
				
				// Clear all spots
				for ( Spot s : spots ) {
					s.clear();
				}
				
				// Reset state variables
				currentLayout = null;
				fileUpToDate = true;
				
				// To be really really sure...
				System.gc();
			}
		});
		mnFile.add(mntmNew);
		
		// - - Open Layout
		JMenuItem mntmOpen = new JMenuItem("Open Soundboard Layout...");
		mntmOpen.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_MASK));
		mntmOpen.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// Confirm clearing of the board, return if Cancel is pressed
				if ( ! confirmClear() ) return;
				
				// Create a file chooser
				JFileChooser chooser = new JFileChooser();
				
				// If we have a previous directory stored, go there
				if ( lastLayoutPath != null ) {
					chooser.setCurrentDirectory(lastLayoutPath);
				}
				
				// Set the file filter
				chooser.setFileFilter(LAYOUT_FILE_FILTER);
				
				// Show the chooser, capture the response
				int res = chooser.showOpenDialog(SDPMain);
				
				// If Cancel was pressed, stop here
				if ( res == JFileChooser.CANCEL_OPTION ) return;
				
				// Otherwise, load the layout (in another thread, because it's SLOW!)
				new Thread(new Runnable() {
					public void run() {
						BoardLayout.loadFromFile(chooser.getSelectedFile(), spots);
					}
				}).start();
				
				// Save the selected folder
				lastLayoutPath = chooser.getCurrentDirectory();
			}
		});
		mnFile.add(mntmOpen);
		
		// - - (Separator)
		mnFile.addSeparator();
		
		// - - Save Layout
		JMenuItem mntmSave = new JMenuItem("Save Soundboard Layout");
		mntmSave.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK));
		mntmSave.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				save();
			}
		});
		mnFile.add(mntmSave);
		
		// - - Save Layout As...
		JMenuItem mntmSaveAs = new JMenuItem("Save Soundboard Layout As...");
		mntmSaveAs.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK));
		mntmSaveAs.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				saveAs();
			}
		});
		mnFile.add(mntmSaveAs);
		
		// - - (Separator)
		mnFile.addSeparator();
		
		// - - Exit
		JMenuItem mntmExitTheConsole = new JMenuItem("Exit the Console");
		mntmExitTheConsole.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_MASK));
		mntmExitTheConsole.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if ( ! confirmClear() ) return;
				System.exit(0);
			}
		});
		mnFile.add(mntmExitTheConsole);
		
		// - Options Menu
		JMenu mnOptions = new JMenu("Options");
		menuBar.add(mnOptions);
		
		JMenuItem mntmSpotPrefs = new JMenuItem("Soundboard Preferences...");
		mntmSpotPrefs.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				SpotPrefs.show();
			}
		});
		mnOptions.add(mntmSpotPrefs);
		
		JMenuItem mntmDeckPrefs = new JMenuItem("Deck Preferences...");
		mntmDeckPrefs.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				DeckPrefs.show();
			}
		});
		mnOptions.add(mntmDeckPrefs);
		
		// Initialize Preferences Dialogs
		SpotPrefs.initialize();
		DeckPrefs.initialize();
	}
	
	// Pop up a file chooser dialog, then load the selected file into the specified deck
	// (Return without loading if Cancel was clicked)
	// Takes the deck index (0 or 1) as its parameter
	public static void loadDeck(int deck) {
		JFileChooser chooser = new JFileChooser();
		
		// Check if a directory was stored from a previous load operation, and if so,
		// move the file chooser to that directory
		if ( SDPConsole.lastDeckPath != null ) {
			chooser.setCurrentDirectory(SDPConsole.lastDeckPath);
		}
		
		// Open the file chooser dialog, and store the response code from the operation
		int response = chooser.showOpenDialog(SDPMain);
		
		// If user clicked Cancel, return without changing anything
		if ( response == JFileChooser.CANCEL_OPTION ) {
			return;
		}
		
		// Otherwise, try to load the file
		try {
			// Load the file into the deck
			decks[deck].load(chooser.getSelectedFile());
			
			// Store the directory the file was loaded from
			lastDeckPath = chooser.getCurrentDirectory();
		}
		// Error messages!
		catch (UnsupportedAudioFileException e) {
			JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "The file you selected is of an unsupported format", "Error Loading Deck", JOptionPane.ERROR_MESSAGE);
		}
		catch (IllegalArgumentException e) {
			JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "The output line could not be opened.\n\nCheck that your audio output is working properly, and that the file is of a supported format.", "Error Loading Deck", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
		}
		catch (IOException e) {
			JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "Could not load the deck: " + e.getMessage() + "\n\nThe file may be corrupt.  Microwave it, then try again.", "Error Loading Deck", JOptionPane.ERROR_MESSAGE);
			e.printStackTrace();
		}
		catch (LineUnavailableException e) {
			JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "The audio line is unavailable for some reason.  Try clubbing your computer.", "Error Loading Deck", JOptionPane.ERROR_MESSAGE);
		}
	}
	
	// Return the main window (so methods from other classes can show dialogs)
	public static JFrame getMainWindow() {
		return SDPMain;
	}
	
	// Display the "Save changes" confirmation message
	// Returns true when the calling method can continue, false if it needs to stop
	public static boolean confirmClear() {
		// Is the file up-to-date?  If so, we don't need to do anything, and the calling method can continue now.
		if ( fileUpToDate ) {
			return true;
		}
		// Otherwise, we continue
		
		int res;
		// Pop the question; use different messages for new file and existing file
		if ( currentLayout == null ) {
			res = JOptionPane.showConfirmDialog(SDPMain, "Save the current soundboard layout?", "Confirm Action", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
		}
		else {
			res = JOptionPane.showConfirmDialog(SDPMain, "Save changes to soundboard layout \"" + currentLayout.getName() + "\"?", "Confirm Action", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
		}
		
		// Check response and act accordingly
		if ( res == JOptionPane.CANCEL_OPTION ) {
			// Cancel was clicked; tell the calling method to stop
			return false;
		}
		else if ( res == JOptionPane.NO_OPTION ) {
			// No was clicked; tell the calling method to go ahead now
			return true;
		}
		else if ( res == JOptionPane.YES_OPTION ) {
			// Yes was clicked; save the file
			save();
			
			// Now the calling method can continue
			return true;
		}
		else {
			// If we make it here, I've done something really dumb...
			JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "Give IfYouLikeGoodIdeas a clubbing - he's done something really dumb!", "Bug Spotted!", JOptionPane.ERROR_MESSAGE);
			return false;
		}
	}
	
	// The "Save As..." function
	public static void saveAs() {
		// Prepare the file chooser
		JFileChooser chooser = new JFileChooser();
		if ( lastLayoutPath != null ) {
			chooser.setCurrentDirectory(lastLayoutPath);
		}
		
		// Set the file filter
		chooser.setFileFilter(LAYOUT_FILE_FILTER);
		
		// Keep trying this until we get what we want (or at least can deal with)
		boolean fileOK = false;
		File finalFile = chooser.getSelectedFile();
		while ( ! fileOK ) {
			// Show the file chooser and collect the response
			int res = chooser.showSaveDialog(SDPMain);
			
			// If user clicked Cancel, return without changing anything
			if ( res == JFileChooser.CANCEL_OPTION ) {
				return;
			}
			
			// Force file extension to be .bla (Board LAyout, or simply "blah")
			String path = chooser.getSelectedFile().toString();
			if ( ! path.endsWith(".bla") ) {
				finalFile = new File(path + ".bla");
			}
			
			// Check if file exists, and confirm overwrite if it does
			if ( finalFile.exists() ) {
				int resp = JOptionPane.showConfirmDialog(SDPMain, "File \"" + finalFile.getName() + "\" already exists!  OK to overwrite?", "Confirm Action", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
				if ( resp == JOptionPane.YES_OPTION ) {
					fileOK = true;
				}
			}
			else {
				fileOK = true;
			}
		}
		
		// Once we're clear, save the file
		BoardLayout.writeToFile(finalFile, spots);
		
		// Update last layout directory
		lastLayoutPath = chooser.getCurrentDirectory();
	}
	
	// The "Save" function
	public static void save() {
		if ( currentLayout == null ) {
			saveAs();
		}
		else {
			BoardLayout.writeToFile(currentLayout, spots);
		}
	}
	
	// Generate a new log file name using the current date
	// Maybe at some point this will have some user-selectable configuration...
	public static String makeNewLogFileName() {
		return "SDP_Playout_Log_" + new SimpleDateFormat("yyyy_MM_dd").format(new Date()) + ".txt";
	}
}
