/**
 * Copyright (c) 2011, Martin Foster <martin@ethereal-realms.org>
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *     * Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of the Ethereal-Realms.org nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.lyra.Host;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

import org.lyra.Ledger;
import org.lyra.Lyra;

/**
 * The Terminus Class deals with establishing the Multimedia Player Daemon
 * connection.  Effectively creating a terminus at our end enabling us to 
 * extract status information and control the player.
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 87%, %LastChangedDate, 28/02/11 12:52PM%
 */
public class Terminus {
	
	/*
	 * Connection information the password is optional and may be set to
	 * null when the constructor is called,
	 */
	private String dHost;
	private Integer dPort;
	private String dPass;
	
	/*
	 * Basic elements of the connection are located here.  Providing a line
	 * based input and output.
	 */
    private Socket dSocket = null;
    private PrintWriter sOut = null;
    private BufferedReader sIn = null;

    // Package classes
    private Ledger Logger;
    
    private int Attempts = 0;
    
    
    
    /*
     * CONSTANTS
     */
    
    // Class Identifier
	private static final String CLASS = "org.lyra.Host.Terminus";
	
	// Number of attempts prior to failing
	private static final int ATTEMPTS = 15; 
	
	/**
	 * Part of the protocol which is normally ignored by this application.
	 */
	public static final String DIRECTORY = "directory:";
	
	
	/**
	 * Part of the protocol which defines a new song entry.
	 */
	public static final String SECTION = "file:";

	
	/**
	 * Separator for key/pair elements.  Used primarily to separate the two
	 * elements into a Map.
	 */
	public static final String SEPARATOR = ": ";
	
	
	/**
	 * Part of the stored playlist listing which defines a new playlist.
	 */
	public static final String PLAYLIST = "playlist:";
	
	
	/**
	 * In multiple reply key/pair elements, this defines how many splits
	 * can be accomplished.
	 */
	public static final int MAX_COL = 2;
	
	
	/**
	 * Rarely used but at times its useful to know what makes a song set unique
	 * which is the existence of the File identifier.
	 */
	public static final String UNIQUE = "file";
	
	
	
	/**
	 * Element that should never be found when doing a host database synch
	 * with our own store.  So we check for it to make sure. 
	 */
	public static final String UPDATING = "updating_db";
	
	
	/**
	 * Certain elements of song information may fall outside what the database
	 * allows.  Note that this would only happen if new elements were added to
	 * the protocol after this version was released.
	 */
	@SuppressWarnings("serial")
	public static final TreeSet<String> ROW_ALLOW = new TreeSet<String>(){
		{
        	add("FILE");
            add("ARTIST");
            add("ALBUM");
            add("TITLE");
            add("GENRE");
            add("TIME");
            add("TRACK");
            add("DATE");
		}
	};
	
	
	/**
	 * Certain elements are required by the database.  This keeps track of
	 * the elements.
	 */
	@SuppressWarnings("serial")
	public static final TreeSet<String> ROW_REQUIRED = new TreeSet<String>(){
		{
        	add("file");
            add("Title");
            add("Time");
		}
	};
	
	
	/**
	 * The protocol is well defined, however in order to reduce changes to the
	 * code if there are changes in the future they have been embedded into a
	 * HashMap.
	 */
    @SuppressWarnings("serial")
	public static final Map<String, String> PROTOCOL = new HashMap<String, String>(){
		{
        	// Status Component
            put("Status.Current","currentsong");
            put("Status.Statistics","stats");
            put("Status.Display","status");
            put("Status.Wait","idle");
            put("Status.NoWait","noidle");
            put("Status.Repeat","replay_gain_status");
            put("Status.Enabled","commands");
            put("Status.Disabled","notcommands");

            
            // Player Options
            put("Mode.Consume","consume");
            put("Mode.Crossfade","crossfade");
            put("Mode.Random","random");
            put("Mode.Repeat","repeat");
            put("Mode.Volume","setvol");
            put("Mode.Single","single");
            put("Mode.Mode","replay_gain_mode");
            
            // Control Features
            put("Player.Next","next");
            put("Player.Pause","pause");
            put("Player.Play","play");
            put("Player.Play.ID","playid");
            put("Player.Previous","previous");
            put("Player.Seek","seekid");
            put("Player.Seek.ID","seekid");
            put("Player.Stop","stop");

            // Current Playlist
            put("Current.Add","add");
            put("Current.Clear","clear");
            put("Current.Delete","delete");
            put("Current.Details","playlistinfo");
            put("Current.Shuffle","shuffle");
            put("Current.Swap","swapid");
            
            // Playlist Management
            put("Playlist.List","listplaylists");
            put("Playlist.Save","save");
            put("Playlist.Remove","rm");
            put("Playlist.Load","load");
            
            // Database
            put("Database.Listing","listallinfo");
            put("Database.Update","update");
            
            // Connection
            put("Connection.Close","close");
            put("Connection.Password","password");
            
            // Protocol
            put("Reply.Okay","OK");
            put("Reply.Fail","ACK");
            put("Error.Clear","clearerror");
            
            // Batch Commands
            put("Batch.Begin","command_list_begin");
            put("Batch.End","command_list_end");
        }
    };

    
    /**
     * Holds the update elements and uses terminology that is hopefully less
     * confusing and consistent with the application.
     */
    @SuppressWarnings("serial")
	public static final Map<String, String> UPDATES = new HashMap<String, String>(){
		{
			put("Database","database");
            put("Library","database");
            put("Update","update");
            put("Playlist","stored_playlist");
            put("Current","playlist");
            put("Player","player");
            put("Mixer","mixer");
            put("Output","output");
            put("Options","options");
        }
    };
    
    
    /**
     * Descriptor which applies to the daemon state when playback is started.
     */
    public static final String STATE_PLAY = "play";
    
    /**
     * Descriptor which applies to the daemon state when playback is paused.
     */
    public static final String STATE_PAUSE = "pause";
    
    /**
     * Descriptor which applies to the daemon state when playback has stopped.
     */
    public static final String STATE_STOP = "stop";
    
    /**
     * Descriptor used for the auditing of entries added to the current playlist.
     */
    public static final String AUDIT_ADDED = "added";
    
    /**
     * Descriptor used for the auditing of entries removed to the current playlist.
     */
    public static final String AUDIT_REMOVED = "removed";
    
    
	/**
	 * Constructor for the class, requires the provision of information related
	 * to the host and an optional password.  A null password will result in
	 * the class ignoring authentication.
	 * <p>
	 * @param logger Ledger instance for logging of faults.
	 * @param host String containing host name or IP address.
	 * @param port String containing the port number.
	 * @param pass String containing the optional password.
	 */
	public Terminus(Ledger logger, String host, String port, String pass) {
		// Pass up package classes
		this.Logger = logger;
		
		// Pass up
		this.dHost = host;
		this.dPort = Integer.decode(port);
		this.dPass = pass;
	}
	// End of Constructor Method

	
	
	/**
	 * As the name implies, deals with the connection to the host establishing
	 * a socket and permitting command/status information to be passed from 
	 * connection onward.
	 * <p>
	 * @return A boolean indicating the success of the operation.
	 */
	public boolean Connect() {
		
        try {
        	
        	/*
        	 * Establish the basic socket connection to the server this should
        	 * prompt the server to send over the version string.
        	 */
            dSocket = new Socket(dHost, dPort);
            sOut = new PrintWriter(dSocket.getOutputStream(), true);
            sIn = new BufferedReader(new InputStreamReader(dSocket.getInputStream()));
            
            // Reply String
            String reply;
            
            /*
             * We need to confirm the version string has been received else
             * the server has must not be having a good day.
             */
            reply = sIn.readLine();
            
            // If the line is different kill it
            if (!reply.startsWith(PROTOCOL.get("Reply.Okay")))
            	return false;
            
            /*
             * The password command is the following element.  We need to
             * confirm that once put into place that the command passed as
             * expected.
             */
            if (dPass != null) {
            	
            	// Place in the password
            	sOut.println(PROTOCOL.get("Connection.Password") + " " + dPass);
            	
            	// Capture output
                reply = sIn.readLine();
                
                // If the line is different kill it
                if (!reply.startsWith(PROTOCOL.get("Reply.Okay")))
                	return false;
            }
            
            // Everything went well
            return true;
        } 
        
        // Host Exceptions are normally associated with DNS error
        catch (UnknownHostException e) {
            Logger.Severe(CLASS, "Connect", e);
        }
        
        // Input output errors are numerous
        catch (IOException e) {
        	Logger.Severe(CLASS, "Connect", e);
        }

        // We failed miserably
        return false;
	}
	// End of Connect Method
	
	
	/**
	 * Prior to exiting we close the connection properly and clean up all
	 * memory handles.
	 */
	public void Disconnect() {

		/*
		 * We have opened a connection with a host and should close up all
		 * associated handles now that we are done.
		 */
		try {
			
			/*
			 * We are polite citizens as a result we will make sure to tell the
			 * sever we are closing.
			 */
			sOut.println(PROTOCOL.get("Connection.Close"));
			
			// We close off the rest of these handles
			sOut.close();
			sIn.close();
			dSocket.close();
		} 
		
		/*
		 * We are not overly concerned about an error in disconnection but
		 * we will make sure to log it in case.
		 */
		catch (IOException e) {
			Logger.Severe(CLASS, "Disconnect", e);
		}
		catch (NullPointerException e) {
			Logger.Severe(CLASS, "Disconnect", e);
		}
    }
	// End of Disconnect Method
	
	
	/**
	 * The recover function is used to recover from IOExceptions that have
	 * been triggered.  Since that typically means a loss of connection it
	 * will attempt to cleanly disconnect and connect once again.
	 * <p>
	 * @param e Exception received and handled.
	 * @return Has the attempt passed or failed. 
	 */
	public boolean Recover(Exception e) {
		
		/*
		 * We essentially keep on going until we connect or we reached a
		 * determined failure rate.
		 */
		if (Attempts++ <= ATTEMPTS) {
			
	        // Log
	        Logger.Warn(CLASS, "Recover", e);
			
			// Cleanly disconnect if possible
			Disconnect();
			
			// Reconnect and reset if applicable
			if (Connect()) {

				// Reset attempts
				Attempts=0;

				// Return true
				return true;
			}
		}
		
		/*
		 * Too many attempts have been made and nothing has been resolved
		 * so we should terminate the application.
		 */
		else {
			
			// Close the application now
			Lyra.closeApplication();
		}
		
		// Fails
		return false;
	}
	// End of Recover Method
	
	
	/**
	 * General method to cover the basics about reading from the protocol in
	 * order to avoid repeating the same lines everywhere.
	 * <p>
	 * @param reply Reply received from the host.
	 * @return Boolean indicating continuation.
	 * @throws IOException 
	 */
	private boolean readResult(String reply) throws IOException {
		
		/*
		 * Elements that return OK do not need to be worried about.  This
		 * should be the normal reply pattern.
		 */
		if (reply.startsWith(PROTOCOL.get("Reply.Okay"))) {
			return false;
		}
			
		/*
		 * This should rarely if ever occur, however if they do we can
		 * simply opt to clear the error and the log will catch the
		 * reply.
		 */
		else if (reply.startsWith(PROTOCOL.get("Reply.Fail"))) {

			// Clear our errors
			writeArgument("Error.Clear");
			
			// Read the reply
			return !readResult();
		}
		
		// Return default
		return true;
	}
	// End of readResult Method
	
	
	
	/**
	 * Basic reading method used to extract all information.  This one will
	 * do nothing more then ensure that the correct termination was sent.
	 * <p>
	 * @return LinkedList containing results line.
	 * @throws IOException 
	 */
	public List<String> readList() throws IOException {
		
		// Create holder collection time
		List<String> lines = new LinkedList<String>();
		
		// Containing line
		String reply;
		
		
		// Loop until complete
		do {
			// Capture
            reply = sIn.readLine();
            
            // Log
            Logger.Fine(CLASS, "readList", reply);
            
            // Append to the list
            lines.add(reply);
		} 
		while (readResult(reply));
		
		// Return results
		return lines;
	}
	// End of readList Method



	/**
	 * The main readList() method does not parse the line as it is extracted
	 * from the socket.   This one however will parse and bring forth a 
	 * singular column.
	 * <p>
	 * @param col Returns elements at specified integer.
	 * @return List of Strings containing split results.
	 * @throws IOException
	 */
	public List<String> readList(int col) throws IOException {
		
		// Create holder collection time
		List<String> lines = new LinkedList<String>();
		
		// Safety measure
		if (col > MAX_COL)
			return lines;
		
		// Containing line
		String reply;
		
		
		// Loop until complete
		do {
			// Capture
            reply = sIn.readLine();
            
            // Log
            Logger.Fine(CLASS, "readList", reply);
            
            // Parse the line
            String[] line = reply.split(SEPARATOR, MAX_COL);
            
            // This will avoid unexpected input
            if (line.length > col)
            	lines.add(line[col]);
		} 
		while (readResult(reply));
		
		// Return results
		return lines;
	}
	// End of readList Method
	
	
	/**
	 * Reads and parses the returned input treating it as a Key/Pair structure
	 * which is returned by elements like STATS and STATUS of the MPD protocol.
	 * <p>
	 * @return Map containing the results set.
	 * @throws IOException
	 */
	public Map<String,String> readMap() throws IOException {
		
		// Holder Type
		Map<String,String> store = new HashMap<String,String>();
		
		// Containing line
		String reply;
		String[] pair;
		
		/*
		 * Unfortunately there seems to be no direct way to drop a key/pair
		 * array directly into a HashMap.  So we need to do a little bit of
		 * conversion in order to return the end type.
		 */
		do {
			// Capture
            reply = sIn.readLine();
            
            // Log
            Logger.Fine(CLASS, "readMap", reply);
            
            // Parse the returned line
            pair = reply.split(SEPARATOR, MAX_COL);
            
            // We can't parse something invalid
            if (pair.length == 2)
            	store.put(pair[0], pair[1]);
		} 
		while (readResult(reply));
		
		// Return
		return store;
	}
	// End of readMap Method
	
	
	/**
	 * This method is used primarily in circumstances where bulk entries are
	 * dropped into place.   Since these commands may not necessarily return
	 * a specific output, this method will just confirm that everything is
	 * good.  It also clears that line from input that can cause problems with
	 * other commands.
	 * <p>
	 * @return boolean indicating result of process.
	 * @throws IOException 
	 */
	public boolean readResult() throws IOException {
		
		/*
		 * We will attempt to capture the line and make sure that the result
		 * of which is acceptable.  If anything fails or the output is not
		 * what we expect it will return false.
		 */
			
		// Containing line
		String reply = sIn.readLine();
            
		// Log
		Logger.Fine(CLASS, "readResult", reply);
			
		// Things went well.
		if (reply.startsWith(PROTOCOL.get("Reply.Okay")))
			return true;

		
		// Return false by default
		return false;
	}
	// End of readResult Method
	
	
	/**
	 * For more complex commands which have a requirement to pass on arguments
	 * useful when dealing with the play lists.
	 * <p>
	 * @param command String containing completed command.
	 * @param args String array containing arguments to pass.
	 */
	public void writeArgument(String command, String... args) {

		// Collecting element
		StringBuffer sb = new StringBuffer();
		
		// Loop and append arguments
		for (String arg: args)
			sb.append(" " + arg);
			
		// Safety ensure the command exists
		if (PROTOCOL.containsKey(command))
			sOut.println(PROTOCOL.get(command) + sb.toString());
	}
	// End of writeArgument Method
	
	
	/**
	 * Method which will write a specific series of commands to the socket for
	 * processing by the host.  Mostly used for status information updates as
	 * they will not require the use of arguments.
	 * <p>
	 * @param command String array of commands which will be sent.
	 */
	public void writeBatch(String...command) {
		
		/*
		 * This method will ignore any keys which are not present in the
		 * PROTOCOL constant.  No meaningful error is returned and should be 
		 * a clue if the system is not sending a desired command over the
		 * wire.
		 */
		for (int count=0; count < command.length; count++)
			if (PROTOCOL.containsKey(command[count]))
				sOut.println(PROTOCOL.get(command[count]));
	}
	// End of writeBatch Method

}
