/* ************************************************************************
#
#  DivConq
#
#  http://divconq.com/
#
#  Copyright:
#    Copyright 2012 eTimeline, LLC. All rights reserved.
#
#  License:
#    See the license.txt file in the project's top-level directory for details.
#
#  Authors:
#    * Andy White
#
************************************************************************ */
package divconq.lang;

import java.util.HashMap;

import org.joda.time.DateTime;

import divconq.db.IDatabaseManager;
import divconq.db.ObjectCallback;
import divconq.db.ObjectResult;
import divconq.db.QueryRequest;
import divconq.db.mumps.DatabaseManager;
import divconq.locale.LocaleUtil;
import divconq.locale.Localization;
import divconq.log.Logger;
import divconq.scheduler.Scheduler;
import divconq.schema.SchemaManager;
import divconq.struct.FieldStruct;
import divconq.struct.RecordStruct;
import divconq.work.IRunnable;
import divconq.work.TaskInfo;
import divconq.work.WorkPool;
import divconq.xml.XElement;
 
/**
 * Hub is the center of activity for DivConq applications.  Most of the built-in resources/features are available via the Hub.
 * The Hub must be initialized by creating a HubResource and passing it to "start".  When the application quits it is best
 * to call "stop".
 * 
 * There is only one Hub object per process (per JVM), the way to access the Hub object is through Hub.instance.
 * 
 *   @see divconq.test.TestDb for an example of how to start and stop the Hub
 *  
 * @author Andy
 *
 */
public class Hub {
	protected long starttime = System.currentTimeMillis();
	protected Clock clock = new Clock();

	// TODO
	//protected Bus bus = new Bus();
	protected WorkPool workpool = new WorkPool();
	protected Scheduler scheduler = new Scheduler();
	protected HashMap<String, IDatabaseManager>  dbman = new HashMap<String, IDatabaseManager>();  
	
	protected HubResources resources = null;		
	
	/*
	public Bus getBus() {
		return this.bus;
	}
	*/
	
	/**
	 * DivConq uses a specialized type system that provides type consistency across services 
	 * (including web services), database fields and stored procedures, as well as scripting.
	 * 
	 * All scalars (including primitives) and composites (collections) are wrapped by some
	 * subclass of Struct.  List/array collections are expressed by this class.  
	 * This class is analogous to an Array in JSON but may contain type information as well, 
	 * similar to Yaml.
	 * 
	 * There are schema files (written in Xml and stored in the Packages repository) that define
	 * all the known data types, including complex data types.
	 * 
	 * @return the master collection of all known simple and complex data types
	 */
	public SchemaManager getSchema() {
		if (this.resources != null)
			return this.resources.getSchema();
		
		return null;
	}
	
	/**
	 * All translation/globalization data is stored in the dictionary.  There are dictionary files
	 * (written in Xml and stored in the Packages repository) that define the localization tokens.
	 *  
	 * @return master collection of all languages and tokens
	 */
	public Localization getDictionary() {
		if (this.resources != null)
			return this.resources.getDictionary();
		
		return null;
	}
	
	/**
	 * The WorkPool is a general purpose thread pool that should be used to execute tasks.
	 * DivConq does not recommend using your own threads as you'll lose the TaskContext and
	 * many features will cease to work correctly.  However, do not use WorkPool to run tasks that
	 * are known to block for long periods (seconds).  WorkPool is for tasks that execute non-stop
	 * and that use async calls when encountering a potentially blocking situation.  (note the
	 * async result should come back on a WorkPool thread also - not necessarily the same
	 * thread).
	 * 
	 * @see TaskContext for addition notes.
	 *   
	 * @return the WorkPool manager
	 */
	public WorkPool getWorkPool() {
		return this.workpool;
	}
	
	/**
	 * Along the same lines as WorkPool (see "getWorkPool") the scheduler provides a way to 
	 * run tasks.  Scheduler uses the WorkPool for running tasks.  However, Scheduler has 
	 * features to schedule the task at some future time and to optionally make the task recurring.
	 * 
	 * See the "schedule" methods in Clock for an alternative.  Scheduler is best for tasks that
	 * need to retain TaskContext (which is often) and that occur once or every 10 seconds or more.
	 * Tasks that need to occur very frequently or need to be on the system clock may be candidates 
	 * for Clock scheduling.  
	 * 
	 * @return the scheduling manager
	 */	
	public Scheduler getScheduler() {
		return this.scheduler;
	}
	
	/**
	 * The clock tracks the application's time (time zone and date time).  The application's time
	 * can be altered or even sped up.  There are also some scheduling methods in Clock but consider
	 * using Scheduler over Clock, see "getScheduler".
	 * 
	 * @return the clock manager
	 */
	public Clock getClock() {
		return this.clock;
	}
	
	/**
	 * The whole of Hub is started based off the settings from a single config file.  
	 * 
	 * @return the root element of the Hub config file
	 */
	public XElement getConfig() {
		if (this.resources != null)
			return this.resources.getConfig();

		return null;
	}

	/**
	 * @return time the Hub started in ms since 1970 
	 */
	public long getStartTime() {
		return this.starttime;
	}

	/**
	 * DivConq has built-in database support for a feature we call dcDb, a database based on M (MUMPS).  
	 * M is a noSQL database, but it can be used to provide relational like tables in addition to
	 * specialized data structures.  It is dcDb's goal to provide a solution for all small/medium data
	 * persistence needs.
	 * 
	 * However, dcDb is an optional feature and DivConq framework may be used without it.
	 *    
	 * @return the default database connection, if any
	 */
	public IDatabaseManager getDatabase() {
		return this.dbman.get("default");
	}

	/**
	 * DivConq has built-in database support for a feature we call dcDb, a database based on M (MUMPS).  
	 * M is a noSQL database, but it can be used to provide relational like tables in addition to
	 * specialized data structures.  It is dcDb's goal to provide a solution for all small/medium data
	 * persistence needs.
	 * 
	 * However, dcDb is an optional feature and DivConq framework may be used without it.
	 *
	 * @param id the id of the database connection to use 
	 * @return the database connection for that id
	 */
	public IDatabaseManager getDatabase(String id) {
		return this.dbman.get(id);
	}
	
	/**
	 * @return the HubResources object that was used to start the Hub 
	 */
	public HubResources getResources() {
		return this.resources;
	}
	
	/**
	 * Before many of the features of DivConq can be used the Hub must be started.  This requires that a HubResources
	 * object be created and hold valid paths/config/etc.
	 * 
	 * @param resources the resources to use in startup
	 * @return a log of the startup process, see "hasErrors" and such in OperationResult
	 */
	public OperationResult start(HubResources resources) {
		this.resources = resources;
		
		// in case resources have not be initialized, do so
		// OperationResult will be set to the Hub Task Context is all is well
		OperationResult or = resources.init();
		
		if (or.hasErrors()) {
			or.exit(113, "Unable to continue, hub rescources not properly initialized");
			return or;
		}
		
		TaskContext.getHubContext().setLocale(LocaleUtil.getDefaultLocale());
		
		XElement config = this.resources.getConfig();
		
		// prepare the logger - use files, use custom log writer
		Logger.init(config.find("Logger"));
		
		or.boundary("Origin", "hub:", "Op", "Start");		
		
		or.info(0, "Using hub id: " + TaskContext.getHubId());
		
		or.info(0, "Starting Clock");
		
		this.clock.init(config.find("Clock"));
		this.clock.start();
		
		or.info(0, "Starting Work Pool");
		
		this.workpool.init(config.find("WorkPool"));
		this.workpool.start();
		
		or.info(0, "Initializing scheduler");

		this.scheduler.init(config.find("Scheduler"));
		
		/*
		tc.logInfo("Initializing Bus");
		
		// setup our bus
		this.bus.init(hubname, this.config.find("Bus"));
		*/
		
		or.info(0, "Initializing Databases");
		
		for (XElement dbconf : config.selectAll("Database")) {
			String id = dbconf.getAttribute("Id", "default");
			
			or.info(0, "Allocating Database Manager using id: " + id);
			
			// TODO get class from config
			DatabaseManager dbm = new DatabaseManager();  
			dbm.init(dbconf);
			dbm.start();
			
			this.dbman.put(id, dbm);
		}

		// TODO check for repo updates if connected via db and if in Production mode (not dev mode)
		// if true then restart with resume code
		//this.downloadRepo();
		
		// TODO if not connected to a database then prep so that once hub is connected we check for updates
		
		/*
		tc.logInfo("Loading modules");
		
		// TODO make sure we don't resolve any resource above the FC base
		for (XElement el : this.config.selectAll("Module")) {
			tc.logInfo("Loadnig module: " + el.getAttribute("Name"));
			
			ModuleLoader loader = new ModuleLoader(Hub.class.getClassLoader());
			loader.init(el);		
			this.modules.put(loader.getName(), loader);
			this.orderedModules.add(loader);
			loader.start();
		}
		*/
		
		or.info(0, "Starting scheduler");
		
		this.scheduler.start();
		
		// TODO temporary until we get scheduler working
		// every five minutes run cleanup to remove expired temp globals
		this.scheduler.runEvery(new IRunnable() {
			@Override
			public void run(final TaskInfo ctx) {
				RecordStruct params = new RecordStruct(
						new FieldStruct("ExpireThreshold", new DateTime().minusMinutes(5)),
						new FieldStruct("LongExpireThreshold", new DateTime().minusMinutes(30))
				);
				
				Hub.instance.getDatabase().submit(new QueryRequest("dcCleanup", params), new ObjectCallback() {					
					@Override
					public void process(ObjectResult res) {
						if (res.hasErrors())
							ctx.context().errorTr(114);
					}
				});
			}
		}, 300);		
		
		or.info(0, "Hub started");
		
		or.boundary("Origin", "hub:", "Op", "Run");
		
		return or;
	}

	/**
	 * Gracefully stop the scheduler, database connections, workpool, logger and such
	 * 
	 * @return a log of the shutdown process
	 */
	public OperationResult stop() {
		TaskContext.useHubContext();

		OperationResult or = new OperationResult(true);
		
		or.boundary("Origin", "hub:", "Op", "Stop");
		
		or.info(0, "Stopping hub");
		
		or.info(0, "Stopping scheduler");
		
		// TODO tell surface modules not to accept new requests - enter a slow and careful come down  
		
		this.scheduler.stop();
		
		/*
		tc.logInfo("Stopping modules");
		
		for (int i = this.orderedModules.size() - 1; i >= 0; i--) {
			ModuleLoader mod = this.orderedModules.get(i);			
			tc.logInfo("Stopping module: " + mod.getName());			
			mod.stop();
		}
		*/
		
		or.info(0, "Stopping logger");		
		Logger.stop();
		
		or.info(0, "Stopping work pool");
		
		this.workpool.stop();
		
		or.info(0, "Stopping database");
		
		for (IDatabaseManager dbm : this.dbman.values())
			dbm.stop();
		
		/*
		tc.logInfo("Stopping bus");
		
		// TODO add
		this.bus.stop();
		*/
		
		or.info(0, "Stopping clock");
		
		this.clock.stop();
		
		or.info(0, "Hub stopped");
		
		return or;
	}
	
	// TODO
	// return true if update was needed
	public boolean downloadRepo() {
		// TODO make this a setting - developer probably do not want auto updates from repo
		
		// TODO check against "repo" (fall back to "default" if no "repo") database too see if we are up to date (hash file)
		// if not then load everything and restart with resume code
		
		return false;
	}
	
	/*
	public String getLibraryPath(String libraryName, String alias) {
		if (this.libpaths == null) {
			this.libpaths = System.getProperty("java.class.path").split(";");
			
			// if this is UNIX/Linux then split on ':' instead
			if (this.libpaths.length == 1) 
				this.libpaths = System.getProperty("java.class.path").split(":");
		}
		
		String retpath = null;
		
		for (String path : this.libpaths) {
			if (path.contains(File.separatorChar + libraryName + ".jar")) {
				retpath = path;
				break;
			}
			
			if (path.contains(File.separatorChar + libraryName + File.separatorChar)) {
				retpath = path + "/";
				break;
			}
		}
		
		if (retpath == null) {
			try {
				// try some predictable places
				File proj = new File("./" + libraryName + "/bin");
				
				if (proj.exists()) 
					retpath = proj.getCanonicalPath() + "/";
				else {
					File jar = new File("./lib/" + libraryName + ".jar");
					
					if (jar.exists()) 
						retpath = jar.getCanonicalPath();
				}
			}
			catch (Exception x) {
				
			}
		}
		
		if ((retpath == null) && !StringUtil.isBlank(alias)) {
			try {
				// try some predictable places
				File proj = new File("./www/" + alias + "/build");
				
				if (proj.exists()) 
					retpath = proj.getCanonicalPath() + "/";
			}
			catch (Exception x) {
				
			}
		}
		
		if (retpath != null)
			retpath = retpath.replace("\\", "/");
		
		return retpath;
	}
	*/

	// ============ STATIC ============
	
	static public Hub instance = new Hub();
	
	static {
		// java 6 does not do TTL correctly, change it
		java.security.Security.setProperty("networkaddress.cache.ttl" , "0");		
	}
	
	/*
	static public void main(final String[] args) {
		Hub hub = new Hub();
		
		DaemonContext dt = new DaemonContext() {
			protected String[] mainargs = args;
			
			@Override
			public DaemonController getController() {
				return null;
			}
			
			@Override
			public String[] getArguments() {
				return this.mainargs;
			}
		};
		
		try {
			System.out.println("Starting Server");
			hub.init(dt);
			hub.start();
			
			// if we are coming from Eclipse then start will hold the main thread until we are done, so next step is to quit
			
			hub.stop();
			hub.destroy();
		}
		catch (Exception x) {
			System.out.println("CLI error: " + x);
		}

		System.exit(1);
	}

	@Override
	public void destroy() {
		this.procCtx = null;
		Hub.instance = null;
	}

	@Override
	public void init(DaemonContext ctx) throws DaemonInitException, Exception {
		System.out.println("Initializing Hub");
		
		// java 6 does not do TTL correctly, change it
		java.security.Security.setProperty("networkaddress.cache.ttl" , "0");
		
		Hub.instance = this;
		this.procCtx = ctx;
	}
	*/
}
