/**
 * 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.Database;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.swing.SwingUtilities;

import org.lyra.AbstractBlock;
import org.lyra.Ledger;
import org.lyra.Block.NullBlock;
import org.lyra.Database.Comptroller;
import org.lyra.Host.Status;
import org.lyra.Host.Terminus;
import org.lyra.Host.UpdateEvent;
import org.lyra.Host.UpdateListener;

/**
 * Deals with the basic status handling of the Multimedia Player Daemon
 * protocol and its interaction with the rest of the system.  
 * <p>
 * @author Martin Foster
 * @version 1.0.1, %Revision, 99%, %LastChangedDate, 17/03/11 8:17PM%
 */
public class Library implements Runnable, UpdateListener {

	// Package Classes
	private Ledger Logger;
	private Comptroller Manager;
	private Terminus Host;
	private AbstractBlock Block;
	private Status Information;
	
	// Settings and languages
	private Map<String,String> Lang;
	
	// Thread instance
	private Thread Synch = null;
	
	// Constants
	private static final int ENTRIES = 10;
	private static final String CLASS = "org.lyra.Host.Library";
	
	
	/**
	 * Primary constructor for this class.   Primarily responsible for passing
	 * up the base Classes for management and generating the initial maps.
	 * <p>
	 * @param logger Ledger instance for logging and debugging purposes.
	 * @param manager Comptroller instance for interacting with the database.
	 * @param host Terminus instance for direct interaction with the Host.
	 * @param block Blocking capability instance.
	 * @param information Status information from the Music Player Daemon.
	 * @param lang Map containing language elements.
	 */
	public Library(Ledger logger, Comptroller manager, Terminus host, AbstractBlock block, Status information, Map<String,String> lang) {

		// Pass Up
		this.Logger      = logger;
		this.Manager     = manager;
		this.Host        = host;
		this.Block       = block;
		this.Information = information;
		this.Lang        = lang;
		
		
		/*
		 * This update does not create an interface but will be used to synch
		 * up the changes between the database and the daemon.
		 */
		syncLibrary();
	}
	// End of Constructor Class
	
	
	/**
	 * This is an alternate constructor used when there is no need to block a
	 * user interface.
	 * <p>
	 * @param logger Ledger instance for logging and debugging purposes.
	 * @param manager Comptroller instance for interacting with the database.
	 * @param host Terminus instance for direct interaction with the Host.
	 * @param information Status information from the Music Player Daemon.
	 * @param lang Map containing language elements.
	 */
	public Library(Ledger logger, Comptroller manager, Terminus host, Status information, Map<String,String> lang) {
		// Pass Up
		this.Logger      = logger;
		this.Manager     = manager;
		this.Host        = host;		
		this.Information = information;
		this.Lang        = lang;
		
		// Generate elements
		this.Block = new NullBlock();
		

		/*
		 * This update does not create an interface but will be used to synch
		 * up the changes between the database and the daemon.
		 */
		syncLibrary();
	}
	// End of Constructor Class
	
	
	/**
	 * Manages the database of songs stored on our local database and the host
	 * daemon.  Attempts to make the overall transition more straight forward.
	 */
	public void syncLibrary() {
		
		// We can only proceed if there is only one instance
		if (Synch == null) {
			
			// Create new thread and start
			Synch = new Thread(this);
			Synch.start();
			
			/*
			 * It may seem a bit unusual to spawn a thread and then wait
			 * for the element to work until it is complete.  However this
			 * will permit concurrent UI drawing and synch processing.
			 */
			try {
				
				// Join the process
				Synch.join();
				
				// Reset for another pass
				Synch = null;
			} 
			
			/*
			 * This thread has been interrupted, so we will make sure that
			 * the block is removed.
			 */
			catch (InterruptedException e) {
				
				/*
				 * Ensure that this process takes place within the swing
				 * thread.
				 */
				SwingUtilities.invokeLater(new Runnable() {
					
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						Block.stopBlock();
					}
				});
				
				// Reset for another pass
				Synch = null;
			}
		}
	}
	// End of syncLibrary Method
	
	
	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		
		/* 
		 * The first thing we have to do is establish the fact that an update
		 * is in fact required.   So we pull the State map from  Host.Status
		 * and compare them with what we have.
		 */
		
		// Extract Database Statistics
		Map<String,Integer> database = Manager.getHostStatistics();
		
		// Extract Server Statistics
		Map<String,String> server = Information.getStatistics();
		
		
		/*
		 * Quick comparison check to determine the flow of information and
		 * where elements will fall into.
		 */
		for (String key: database.keySet()) {
			
			// All the checks are the same so may as well loop
			if (database.get(key) != Integer.parseInt(server.get(key))) {
				
				/*
				 * Now that we know we will be updating the document it is time
				 * to start blocking the interface.
				 */
				SwingUtilities.invokeLater(new Runnable() {
					
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						Block.startBlock();
					}
				});

				
				/*
				 * Log occurrence of failed match, also record which entry
				 * failed the comparison check.
				 */
				Logger.Info(
					CLASS, 
					"syncLibrary", 
					Lang.get("Sync.Start").replaceAll("MCAUSE", key)
				  );

				
				/*
				 * Here we begin the process of actually going through and
				 * confirming if an element is already in the database.  This
				 * also confirms that there are no changes to the information
				 * which may be critical for the synchronisation process.
				 */
				
				// Ensure everything goes as planned
				try {
					
					// Update block message
					Block.setMessage(Lang.get("Host.Sync.Message.Host"));
					
					// Time to request the change in details
					Host.writeBatch("Database.Listing");
					
					// Extract all of the elements
					List<String> list = Host.readList();
					
					/*
					 * If the list returned has less then 10 elements then we can
					 * be assured there is not even one entry in the database
					 * and should as a result not continue with the sync
					 * process.
					 */
					if (list.size() > ENTRIES) {
						
						// Library holder
						Map<String,Map<String,String>> library
						  = Manager.getHostLibrary();
						
						// Holder Class
						Map<String,String> rows = new HashMap<String,String>();
						Map<String,String> song;
						
						// Complex operation types
						List<Map<String,String>> insert = new LinkedList<Map<String,String>>();
						List<Map<String,String>> update = new LinkedList<Map<String,String>>();;
						
						
						// Update block message
						Block.setMessage(Lang.get("Host.Sync.Message.Compare"));
						
						/*
						 * The terminus class will deal with returning the lines
						 * untouched.  However there is a lot of additional 
						 * processing to do.
						 */
						while (list.size() > 0) {
							
							// Always remove the first element
							String line = list.remove(0);
							
							// We do not care about directories
							if (line.startsWith(Terminus.DIRECTORY))
								continue;
							
							
							/*
							 * One of the core elements that will be returned
							 * will be a few file identifier.   When that line
							 * has been reached the processing will begin.
							 */
							if ((line.startsWith(Terminus.SECTION))
							 && (rows.size() > 0)){
								
								/* 
								 * Here we have attempted to get information from
								 * the database and got no results.  This means
								 * that the song is in fact new.
								 */
								if (!library.containsKey(rows.get(Terminus.UNIQUE))) {
									
									// Add this to the appropriate hash
									insert.add(rows);
								
									// Flag the addition
									Logger.Info(
										CLASS,
										"syncLibrary",
										Lang.get("Sync.Insert").replaceAll("MFILE", rows.get("file"))
									  );
								}
								
								/*
								 * Now we start the process of actually going
								 * through the keys and determining if they
								 * are similar or not.
								 */
								else {
									
									// Extract details since we need song information
									song = library.get(rows.get(Terminus.UNIQUE));
									
									// Loop through
									for (String iKey: rows.keySet()) {
										
										// Skip a certain key
										if (iKey.equals(Terminus.UNIQUE))
											continue;
										
										// Normal operation
										if ((song.containsKey(iKey)) 
										 && (rows.containsKey(iKey))
										 && (song.get(iKey) != null)
										 && (!song.get(iKey).equals(rows.get(iKey)))) {
											
											// Add this to appropriate hash
											update.add(rows);
											
											// Log change
											Logger.Info(
												CLASS,
												"syncLibrary",
												Lang.get("Sync.Update").replaceAll("MFILE", rows.get("file"))
											  );
											
											// Force out of loop
											break;
										}
									}
									
									/*
									 * We remove the entry from the library element
									 * itself.  What is left will be stale entries 
									 * that exist outside of the host.
									 */
									library.remove(rows.get(Terminus.UNIQUE));
								}
								
								// Reset for another run
								rows = new HashMap<String,String>();
							}
							
							
							/*
							 * Otherwise we just do a nice and simple parse job 
							 * and pass on the results as a hash.
							 */
							String[] cols = line.split(Terminus.SEPARATOR, Terminus.MAX_COL);

							// Quick safety check
							if (cols.length > 1)
								rows.put(cols[0], cols[1]);
						}
						
						/*
						 * Now that we have generated the appropriate data types, we
						 * can now send them to the manager to be dealt with.
						 */
						if (insert.size() > 0) {
							
							// Update block message
							Block.setMessage(Lang.get("Host.Sync.Message.Insert"));
							
							// Update database
							Manager.insertSongs(insert);
						}
						
						// Update modified entries
						if (update.size() > 0) {
							
							// Update block message
							Block.setMessage(Lang.get("Host.Sync.Message.Update"));
							
							// Update database
							Manager.updateSongs(update);
						}
						
						// Whatever is left in the library we remove
						if (library.size() > 0) {
							
							// Update block message
							Block.setMessage(Lang.get("Host.Sync.Message.Delete"));
							
							// Update database
							Manager.deleteSongs(library.keySet());
						}	
							
						
						/*
						 * All we have left to do now is to update the database
						 * with statistics taken from the server.    We do not
						 * add up the numbers ourselves since the numbers may
						 * not be calculated the same was as the server.
						 */
						
						// Update block message
						Block.setMessage(Lang.get("Host.Sync.Message.Statistics"));
						
						// Update the statistics
						Manager.updateHostStatistics(database.keySet(), server);
					}
				} 
				
				// A failure in the connection
				catch (IOException e) {
					// User configured logging
					Logger.Severe(CLASS, "syncLibrary", e);
				}	
				
				
				/*
				 * Ensure that this process takes place within the swing
				 * thread.
				 */
				SwingUtilities.invokeLater(new Runnable() {
					
					/* (non-Javadoc)
					 * @see java.lang.Runnable#run()
					 */
					public void run() {
						Block.stopBlock();
					}
				});
				
				// Return to avoid unnecessary messages
				return;
			}
			
			// Log the fact that everything is good
			Logger.Info(
				CLASS,
				"syncLibrary",
				Lang.get("Sync.Skip")
			 );	
		}
	}
	// End of run Method

	
	/* (non-Javadoc)
	 * @see org.lyra.Host.UpdateListener#hostUpdated(org.lyra.Host.UpdateEvent)
	 */
	public void hostUpdated(UpdateEvent update) {
		
		/*
		 * Once we get the notification that things have changed, we will need
		 * to sync up the database even while the user is making use of it.
		 */
		if ((update.isDatabase())
		 || (update.isLibrary())) {
			
			/*
			 * We check to make sure that the update process is not in fact
			 * necessary.  If the entry does not exist then the process is
			 * no longer in progress and we are free to update.
			 */
			if (Information.getState().get(Terminus.UPDATING) == null)
				syncLibrary();
			
		}
	}
	// End of hostUpdated Method
}
