/**
 * 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 java.util.regex.Matcher;

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 Music Player Daemon
 * protocol and its interaction with the rest of the system.  
 * <p>
 * @author Martin Foster
 * @version 1.0.4, %Revision, 134%, %LastChangedDate, 24/09/11 1:47AM%
 */
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 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) {
			
			/*
			 * Now that we know we will be updating the document it is time
			 * to start blocking the interface.
			 */
			Block.startBlock();

			
			// 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();
			} 

			// We really do not care about a fault in this
			catch (InterruptedException e) {}
			
			// We will always stop the block prior to completion
			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))) {
				
				/*
				 * 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"));
					

					// Library holder
					Map<String,Map<String,String>> library
					  = Manager.getHostLibrary();
					
					// Extract number of elements
					int elements = library.size();
					
					// 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>>();;
					
					// Holder for line
					String line;
					
					// Update block message
					Block.setMessage(Lang.get("Host.Sync.Message.Compare"));
					
					// Time to request the change in details
					Host.writeBatch("Database.Listing");
					
					/*
					 * The terminus class will deal with returning the lines
					 * untouched.  However there is a lot of additional 
					 * processing to do.
					 */
					do {
						
						// Read Line for Another Go
						line = Host.readLine();
						
						/*
						 * 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 == null) 
						  || (line.startsWith(Terminus.SECTION)))
						 && (rows.size() > 0)) {
							
							// Event string default for logging
							String event = Lang.get("Sync.Skip");
							
							/* 
							 * 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
								event = Lang.get("Sync.Insert");
							}
							
							/*
							 * 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);
										
										// Flag the addition
										event = Lang.get("Sync.Update");
										
										// 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));
							}
							
							// Cleanse filename
							String cleaned = 
								Matcher.quoteReplacement(rows.get("file"));
							
							// Log change
							Logger.Info(
								CLASS,
								"syncLibrary",
								event.replaceAll("MFILE", cleaned)
							  );
							
							// Reset for another run
							rows = new HashMap<String,String>();
						}
									
						
						/*
						 * Directories are not something we want to deal with
						 * as they have no real relevance to the application
						 * and would simply complicate matters.
						 */
						if ((line != null) 
						 && (!line.startsWith(Terminus.DIRECTORY))) {
							
							/*
							 * Parse the line and append to the the hash we are
							 * building up for use.
							 */
							String[] cols = line.split(
									Terminus.SEPARATOR, 
									Terminus.MAX_COL
								  );

							// Quick safety check
							if (cols.length > 1)
								rows.put(cols[0], cols[1]);
						}
												
					} while (line != null);
										
					/*
					 * 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() != elements) 
					 && (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
					  );
				}	
				
				// 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()) {
			
			/*
			 * 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
}
