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 defines a "spot", consisting of an audio clip and a button to trigger it

import java.awt.Color;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;

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.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.LineBorder;

public class Spot implements ActionListener {
	private JButton button;
	private JLabel counter;
	private Clip clip = null;
	private Timer timer;
	private LineListener timerCtrl;
	
	private Color spotColor = null;
	
	// String data for use with save/load mechanism
	private String name = "";
	private String url = "";
	
	// Constants
	private static final LineBorder PLAYING_BORDER = new LineBorder(Color.GREEN, 2);
	static final int COLOR_CUTOFF = 410;
	static final int GREEN_CUTOFF = 230;
	
	// Return this spot's button object
	public JButton getButton() {
		return button;
	}
	
	// Return the label object for this spot's counter
	public JLabel getCounter() {
		return counter;
	}

	// Constructor takes in the index number of the new spot object, so that it can be positioned correctly
	public Spot(int i) {
		// Create button and counter objects
		button = new JButton("-");
		counter = new JLabel("0:00.0");
		
		// Build the right-click menu
		JPopupMenu menu = new JPopupMenu();
		
		// - Load
		JMenuItem load = new JMenuItem("Load Spot...");
		load.setActionCommand("load"); // To tell it apart from color...
		// Set the action listener to this Spot object (since Spot implements ActionListener...)
		// Hey, at least this particular hack seems to be widely known and accepted! [football helmet]
		// (This hack allows us to use the "this" keyword in the event handler method to reference the Spot)
		load.addActionListener(this); 
		menu.add(load);
		
		// - Set Color
		JMenuItem color = new JMenuItem("Set Color...");
		color.setActionCommand("color"); // To tell it apart from load...
		color.addActionListener(this); 
		menu.add(color);
		
		// - Clear
		JMenuItem clear = new JMenuItem("Clear");
		clear.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				clear();
			}
		});
		menu.add(clear);
		
		// - Add the menu to the button
		button.setComponentPopupMenu(menu);
		
		// Create the counter's timer
		timer = new Timer(100, new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				if ( clip != null ) {
					updateCounter(clip.getMicrosecondLength() - clip.getMicrosecondPosition());
				}
			}
		});
		// Create the timer's event handler (that actually also controls the button border...)
		timerCtrl = new LineListener() {
			public void update(LineEvent e) {
				if ( e.getType() == LineEvent.Type.START ) {
					button.setBorder(PLAYING_BORDER);
					timer.start();
				}
				else if ( e.getType() == LineEvent.Type.STOP ) {
					button.setBorder(UIManager.getBorder("Button.border"));
					timer.stop();
					updateCounter(clip.getMicrosecondLength());
				}
			}
		};
		
		// Determine the X/Y position of the new spot button based on the index parameter
		int x = 0;
		switch (i % 4) {
			case 0:
				x = 10;
				break;
			case 1:
				x = 108;
				break;
			case 2:
				x = 207;
				break;
			case 3:
				x = 306;
				break;
		}
		int y = 0;
		switch (i / 4) {
			case 0:
				y = 33; // These values were determined through the highly scientific process of guessing...
				break;
			case 1:
				y = 133;
				break;
			case 2:
				y = 233;
				break;
			case 3:
				y = 333;
				break;
		}
		
		// Set the position/size of the button and counter
		button.setBounds(x, y, 89, 89);
		counter.setBounds(x + 36, y + 73, 50, 14);
		
		// Set counter's text alignment and visibility
		counter.setHorizontalAlignment(JLabel.TRAILING);
		counter.setVisible(false);
		
		// Disable the margins on the button so as much text as possible can be displayed
		button.setMargin(new Insets(0, 0, 0, 0));
		
		// Set the button's event handler
		button.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				// If there is actually a clip loaded...
				if ( clip != null ) {
					// If it's playing, stop it
					if ( clip.isActive() ) {
						clip.stop();
					}
					// If it's not playing, cue it to the beginning and play it
					else {
						clip.setFramePosition(0);
						clip.start();
					}
				}
			}
		});
	}
	
	// Event handler function for the button
	public void actionPerformed(ActionEvent e) {
		if ( e.getActionCommand() == "load" ) {
			new SpotLoader(this).setVisible(true);
		}
		else if ( e.getActionCommand() == "color" ) {
			new SpotColorer(this).setVisible(true);
		}
		else {
			// This shouldn't happen
			System.err.println("Spot \"" + name + "\" got invalid action command \"" + e.getActionCommand() + "\"!");
			System.err.println("IfYouLikeGoodIdeas must report to the red courtesy club at once.");
		}
	}
	
	// Load a new clip (from the specified File object) into this Spot with the default color
	public void load(URL url, String name) throws IllegalArgumentException, LineUnavailableException, IOException, UnsupportedAudioFileException {
		load(url, name, null);
	}
	
	// Load a new clip (from the specified File object) into this Spot and set the specified color
	public void load(URL url, String name, Color color) throws LineUnavailableException, IOException, UnsupportedAudioFileException, IllegalArgumentException {
		// If there's a previous clip in the spot, unload it (remove the timer event handler and close it)
		if ( clip != null ) {
			clip.stop();
			clip.removeLineListener(timerCtrl);
			clip.close();
		}
		
		AudioInputStream in;
		
		// Determine whether a cached version of the file can be loaded (if applicable)
		// Build the AudioInputStream from whatever source we choose
		
		// If caching is enabled and the input URL's protocol is HTTP...
		if ( SDPConsole.prefs.getBoolean(SpotPrefs.KEEP_SDP_SPOTS, SpotPrefs.DEFAULT_KEEP_SDP_SPOTS) && url.getProtocol().equals("http") ) {
			// Extract the filename from the URL, then build a File object pointing to a file
			// of that name in the specified SDP spot cache directory
			// http://stackoverflow.com/questions/26508424/need-to-extract-filename-from-url
			String urlStr = url.toString();
			File cacheCandidate = new File(SDPConsole.prefs.get(SpotPrefs.SDP_SPOT_SAVE_LOC, SpotPrefs.DEFAULT_SDP_SPOT_SAVE_LOC), urlStr.substring(urlStr.lastIndexOf('/') + 1));
			
			// If the file doesn't exist...
			if ( ! cacheCandidate.exists() ) {
				// Create the spot download directory (and its parents), if needed
				cacheCandidate.getParentFile().mkdirs();
				
				// Create a SpotDownloadMonitor and show it
				SpotDownloadMonitor monitor = new SpotDownloadMonitor(name);
				monitor.setVisible(true);
				
				// Connect to the SDP server
				HttpURLConnection connection = (HttpURLConnection)url.openConnection();
				
				// If we didn't get a 200 OK response, complain and return
				if ( connection.getResponseCode() != 200 ) {
					JOptionPane.showMessageDialog(SDPConsole.getMainWindow(), "Got a " + connection.getResponseCode() + " " + connection.getResponseMessage() + " error from the SDP server when attempting to download:\n\n" + url.toString() + "\n\nTell Weasel he only had ONE JOB!", "Error Loading Spot", JOptionPane.ERROR_MESSAGE);
					connection.disconnect();
					return;
				}
				
				// We're still here?  Good - let's grab the I/O streams
				InputStream download = connection.getInputStream();
				FileOutputStream save = new FileOutputStream(cacheCandidate);
				
				// Get total size and set it on the monitor window
				monitor.setMax(connection.getContentLength());
				
				// Loop through the input stream and write it to the file
				int bytesRead = -1;
				int totalBytesRead = 0;
	            byte[] buffer = new byte[4096]; // Set buffer size here!
	            while ((bytesRead = download.read(buffer)) != -1) {
	                save.write(buffer, 0, bytesRead);
	                if ( bytesRead > 0 ) {
		                totalBytesRead += bytesRead;
		                monitor.setProgress(totalBytesRead);
	                }
	            }
	            
	            // We're done, close things up
	            save.close();
	            download.close();
	            connection.disconnect();
	            monitor.setVisible(false);
			}
			
			// Finally, load the file from the cache
			in = AudioSystem.getAudioInputStream(cacheCandidate.toURI().toURL());
		}
		// Otherwise, just load directly from the input URL
		else {
			in = AudioSystem.getAudioInputStream(url);
		}
		
		// Load the file and set up decoding
		// (From http://www.javalobby.org/java/forums/t18465.html)
		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)
		);
		AudioInputStream din = AudioSystem.getAudioInputStream(decodedFormat, in);
		
		// Set up output and decode/load the file into memory
        DataLine.Info info = new DataLine.Info(Clip.class, decodedFormat);
        clip = (Clip)AudioSystem.getLine(info);
        clip.open(din);
        
        // Register the event handler (to start/stop the timer)
        clip.addLineListener(timerCtrl);
        
        // Set button text
        // Render as HTML/CSS for better text formatting
        button.setText("<html><body style='width: 72px;'>" + name + "</body></html>");
        
        // Set the counter and show it
        updateCounter(clip.getMicrosecondLength());
        counter.setVisible(true);
        
        // Store the name and URL where we can get them later
 		this.name = name;
 		this.url = url.toString();
 		
 		// Set our color to the one specified
 		setColor(color);
        
        // We should be done with the input streams - close them
        din.close();
        in.close();
	}
	
	// Clear out the current clip and reset to default state
	public void clear() {
		// If there is an actual clip, stop/close it, then get rid of it
		if ( clip != null ) {
			clip.stop();
			clip.removeLineListener(timerCtrl);
			clip.close();
			clip = null;
		}
		setColor(null);
		button.setText("-");
		counter.setVisible(false);
		name = "";
		url = "";
		SDPConsole.fileUpToDate = false;
	}
	
	// Update the counter
	public void updateCounter(long micro) {
		int tenths = (int)((micro / 100000) % 10);
		int sec = (int)((micro / 1000000) % 60);
		int min = (int)((micro / 1000000) / 60);
		counter.setText(min + ":" + String.format("%02d", sec) + "." + tenths);
	}
	
	// Get the current clip's URL
	public String getURL() {
		return url;
	}
	
	// Get the current clip's name (button text)
	public String getName() {
		return name;
	}
	
	// Set this spot's button color
	public void setColor(Color newColor) {
		spotColor = newColor;
		button.setBackground(newColor);
		if ( newColor != null && (newColor.getRed() + newColor.getGreen() + newColor.getBlue() < COLOR_CUTOFF) && newColor.getGreen() < GREEN_CUTOFF ) {
			button.setForeground(Color.WHITE);
			counter.setForeground(Color.WHITE);
		}
		else {
			button.setForeground(Color.BLACK);
			counter.setForeground(Color.BLACK);
		}
		SDPConsole.fileUpToDate = false;
	}
	
	// Return this spot's color
	public Color getColor() {
		return spotColor;
	}
	
	// Return whether or not this spot is loaded (based on whether or not we have a URL stored)
	public boolean isLoaded() {
		return (! url.isEmpty());
	}
}
