/* ************************************************************************
#
#  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.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import divconq.locale.Localization;
import divconq.log.DebugLevel;
import divconq.schema.SchemaManager;
import divconq.util.StringUtil;
import divconq.xml.XElement;
import divconq.xml.XmlReader;

/**
 * Within dcFramework all features are tied together through the Hub class.  To get 
 * the hub going you need to give it access to some resources such as Schema
 * and config.  
 *  
 *  HubResources is the class that ties all the resources together and enables
 *  Hub to start.  A typical application using dcFramework will start up something
 *  like this: 
 *  
 *		HubResources resources = new HubResources("00101", true);
 *		resources.setDebugLevel(DebugLevel.Warn);
 * 		OperationResult or = resources.init();
 *			
 *		if (or.hasErrors()) {
 *			Logger.error("Unable to continue, hub resources not properly initialized");
 *			return;
 *		}
 *	
 *		Hub.instance.start(resources);
 *			
 *	[TODO add link to Quick Start ]
 *			
 *	[TODO add link to Framework Architecture ]
 *  
 * @author Andy
 *
 */
public class HubResources {
	protected String hubid = "00000";
	protected String tier = "private";
	protected String squad = "one";
	protected boolean devmode = false;
	protected DebugLevel startuplevel = DebugLevel.Info;
	
	// post init
	protected boolean initialized = false;
	protected boolean initsuccess = false;
	
	protected List<String> packages = new ArrayList<String>(); 
	protected List<String> reversepackages = new ArrayList<String>(); 
	
	protected XElement config = null;
	
	// TODO
	//protected XElement fabric = null;
	
	protected Localization localization = null;
  	protected SchemaManager schemaman = null;
	
  	/**
  	 * HubId is a 5 digit (zero padded) number that uniquely identifies this Hub (process) in
  	 * the distributed network of Hubs (processes) in your Project (application). 
  	 * 
  	 * @return HubId
  	 */
	public String getHubId() {
		return this.hubid;
	}
	
	/**
  	 * HubId is a 5 digit (zero padded) number that uniquely identifies this Hub (process) in
  	 * the distributed network of Hubs (processes) in your Project (application). 
	 * 
	 * You should only set the HubId once per run, Hubs are not designed to change Ids mid run.
	 * 
	 * @param v HubId
	 */
	public void setHubId(String v) {
		if (StringUtil.isEmpty(v) || (v.length() != 5))
			throw new IllegalArgumentException("Hub id must be 5 digits, zero padded.");
		
		for (int i = 0; i < 5; i++) 
			if (!Character.isDigit(v.charAt(i))) 
				throw new IllegalArgumentException("Hub id must be 5 digits, zero padded.");
		
		TaskContext.setHubId(v);
		this.hubid = v;
	}
	
	/**
	 * Tier indicates not just the greater role of the Hub but also what sort of safety this Hub enjoys.  
	 * By default there are two types:
	 *  
	 * private: this Hub is running on a private, fire-walled, network (company LAN)
	 * public: this Hub is running on a public network such as the Internet or in a DMZ 
	 * 
	 * @return Tier Id
	 */
	public String getTier() {
		return this.tier;
	}

	/**
	 * Tier indicates not just the greater role of the Hub but also what sort of safety this Hub enjoys.  
	 * By default there are two types:
	 *  
	 * private: this Hub is running on a private, fire-walled, network (company LAN)
	 * public: this Hub is running on a public network such as the Internet or in a DMZ 
	 * 
	 * @param v Tier Id
	 */
	public void setTier(String v) {
		this.tier = v;
	}
	
	/**
	 * Squad Id is used to group a number of Hubs together.  A Squad is functionally separated from
	 * all other Squads and may operate independently (disaster recovery).  A Squad may be active 
	 * even while other Squads are, this is not a fail-over architecture but a active-active architecture.
	 * Squads may often be placed in separate data centers or spread across data centers and clouds (IaaS). 
	 * A Squad has its own copy of the Project database, the database is replicated so that each Squad 
	 * will eventually be consistent.
	 * 
	 * @return Squad Id
	 */
	public String getSquadId() {
		return this.squad;
	}
	
	
	/**
	 * Squad Id is used to group a number of Hubs together.  A Squad is functionally separated from
	 * all other Squads and may operate independently (disaster recovery).  A Squad may be active 
	 * even while other Squads are, this is not a fail-over architecture but a active-active architecture.
	 * Squads may often be placed in separate data centers or spread across data centers and clouds (IaaS). 
	 * A Squad has its own copy of the Project database, the database is replicated so that each Squad 
	 * will eventually be consistent.
	 * 
	 * @param v Squad Id
	 */
	public void setSquadId(String v) {
		this.squad = v;
	}
	
	/**
	 * When in Dev Mode the resources are loaded from a local copy of the Repository instead of from 
	 * the config directory.  Developers will debug applications using the local Repository copy 
	 * so that they can edit repository artifacts (schema, dictionary, resource files) in "native"
	 * repository structure.
	 *   
	 * @return Dev Mode
	 */
	public boolean isDevMode() {
		return this.devmode;
	}
	
	/**
	 * When in Dev Mode the resources are loaded from a local copy of the Repository instead of from 
	 * the config directory.  Developers will debug applications using the local Repository copy 
	 * so that they can edit repository artifacts (schema, dictionary, resource files) in "native"
	 * repository structure.
	 *   
	 * @param v Dev Mode
	 */
	public void setDevMode(boolean v) {
		this.devmode = v;
	}
	
	/**
	 * The global (default) Debug Level to use normally comes from the config file, before the config
	 * file is available Hub start-up uses the Debug Level given to this class.   
	 *   
	 * @return Debug Level used during start-up
	 */
	public DebugLevel getDebugLevel() {
		return this.startuplevel;
	}
		
	/**
	 * The global (default) Debug Level to use normally comes from the config file, before the config
	 * file is available Hub start-up uses the Debug Level given to this class.   
	 *   
	 * @param v Debug Level used during start-up
	 */
	public void setDebugLevel(DebugLevel v) {
		this.startuplevel = v;
	}
	
	/**
	 * A list of package names in the order in which the packages are loaded.  Each subsequent package overrides
	 * the previous.  So, for example, the contents of the last package overrides any similar resource in all
	 * the previous packages.
	 *    
	 * @return list of package names
	 */
	public Collection<String> getPackages() {
		return this.packages;
	}
	
	/**
	 * Schema holds the custom data types used by this Project.
	 * 
	 * @return custom data type definitions
	 */
	public SchemaManager getSchema() {
		return this.schemaman;
	}
	
	/**
	 * Dictionary holds a list of Locales for which translations exists for this Project.
	 * 
	 * @return Locales for this Project
	 */
	public Localization getDictionary() {
		return this.localization;
	}

	/**
	 * Config is an XML structure holding the master settings for this Project.  Other configuration
	 * may come from the database.
	 * 
	 * @return master Project settings
	 */
	public XElement getConfig() {
		return this.config;
	}

	/* TODO
	public XElement getFabric() {
		return this.fabric;
	}
	*/

	/**
	 * Manage the resources for this Hub by indicating which hub this is and whether to run 
	 * in developer mode.  See class, HubId and DevMode comments for details.
	 * 
	 * @param hubid Hub Id
	 * @param devmode Development Mode
	 */
	public HubResources(String hubid, boolean devmode) {
		this.setHubId(hubid);
		this.devmode = devmode;
	}
	
	/**
	 * Initialize this object by loading the Schema, Dictionary, Config and Fabric.
	 * 
	 * When in Dev Mode the resources are loaded from a local copy of the Repository instead of from 
	 * the config directory.  Developers will debug applications using the local Repository copy 
	 * so that they can edit repository artifacts (schema, dictionary, resource files) in "native"
	 * repository structure.
	 *   
	 * @return messages logged while initializing this object
	 */
	public OperationResult init() {
		// do not run init twice (not thread safe, should be called by main thread only)
		if (this.initialized) {
			OperationResult or = new OperationResult(true);
			
			if (!this.initsuccess)
				or.error(112, "Hub resources already loaded, but contained errors");
			
			return or;
		}				
		
		this.initialized = true;
		
		// before starting we want to have a valid hub level task context
		// which requires a hub id
		TaskContext.setHubId(this.hubid);
		
		// use the hub (root) task context for logging.  
		// use the startup debug level until we log Logger settings
		TaskContext.useHubContext().setLevel(this.startuplevel);
		
		OperationResult or = new OperationResult(true);
		
		or.info(0, "Loading hub resources in " + (this.devmode ? "developer mode." : "deployment mode"));

		or.trace(0, "Loading packages.xml");
		
		File pkgs = new File(this.devmode ? "./project/packages.xml" : "./conf/packages.xml");
		
		XElement cel = XmlReader.loadFile(pkgs);
		
		if (cel == null) {
			or.error(100, "Unable to load packages.xml file, expected: " + pkgs.getAbsolutePath());
			return or;
		}
		
		for (XElement pack : cel.selectAll("Package")) {
			String n = pack.getAttribute("Name");
			
			this.packages.add(n);
			this.reversepackages.add(0, n);
		}

		or.trace(0, "Packages loaded: " + this.packages);
		
		if (this.devmode) {
			or.trace(0, "Loading config.xml file");
			
			// find the right config file
			File f = new File("./project/config_hub_" + this.hubid + ".xml");
			
			if (!f.exists()) {
				f = new File("./project/" + this.tier + "/config_" + this.squad + ".xml");
			
				if (!f.exists())
					f = new File("./project/" + this.tier + "/config.xml");
			}
			
			if (!f.exists()) {
				or.error(101, "Unable to find config.xml file, expected: " + f.getAbsolutePath());
				return or;
			}
			
			this.config = XmlReader.loadFile(f);
			
			if (this.config == null) {
				or.error(102, "Unable to load config.xml file, expected: " + f.getAbsolutePath());
				return or;
			}
			
			or.trace(0, "Loaded config.xml file at: " + f.getAbsolutePath());
			
			or.trace(0, "Using project compiler to load schema and dictionary");
			
			ProjectCompiler comp = new ProjectCompiler();

			this.schemaman = comp.getSchema(or, this.packages, this.tier);
			
			if (or.hasErrors()) {
				or.exit(103, "Unable to load schema file(s)");
				return or;
			}
			
			or.trace(0, "Schema loaded");
			
			this.localization = comp.getDictionary(or, this.packages, this.tier);
			
			if (or.hasErrors()) {
				or.exit(104, "Unable to load dictionary file(s)");
				return or;
			}
			
			or.trace(0, "Dictionary loaded");

			// TODO get fabric from ./project...
		}
		else {
			or.trace(0, "Loading config.xml file");
			
			File f = new File("./conf/config.xml");
			
			if (!f.exists()) {
				or.error(101, "Unable to find config.xml file, expected: " + f.getAbsolutePath());
				return or;
			}
			
			this.config = XmlReader.loadFile(f);
			
			if (this.config == null) {
				or.error(102, "Unable to load config.xml file, expected: " + f.getAbsolutePath());
				return or;
			}
			
			File confdir = new File("./conf");
			
			or.trace(0, "Loading Schema");
			
			this.schemaman = new SchemaManager();
			
			// TODO make sure that we get in canonical order
			for (File schema : confdir.listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {					
					return (name.startsWith("schema_") && name.endsWith(".xml"));
				}
			})) {
				or.copyMessages(this.schemaman.loadSchema(schema));
			}

			or.copyMessages(this.schemaman.compile());
			
			if (or.hasErrors()) {
				or.exit(103, "Unable to load schema file(s)");
				return or;
			}
			
			or.trace(0, "Schema loaded");
			
			or.trace(0, "Loading Dictionary");
			
			this.localization = new Localization();
			
			// TODO make sure that we get in canonical order
			for (File dict : confdir.listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {					
					return (name.startsWith("dictionary_") && name.endsWith(".xml"));
				}
			})) {
				this.localization.load(or, dict);
			}
			
			if (or.hasErrors()) {
				or.exit(104, "Unable to load dictionary file(s)");
				return or;
			}
			
			or.trace(0, "Dictionary loaded");

			// TODO load into internal structure
			// this.fabric = XmlReader.loadFile("./conf/fabric.xml");
		}
		
		or.info(0, "Hub resources loaded");
		this.initsuccess = true;
		
		return or;
	}
	
	/**
	 * Scan through the local repository or config directory and reload the dictionary
	 * files.  After this any translation used will have updates from the dictionary files.
	 * This method is really only useful in development mode and is not typically called 
	 * by application code.
	 *   
	 * @return messages logged while reloading dictionary
	 */
	public OperationResult reloadDictionary() {
		OperationResult or = new OperationResult(true);
		
		or.trace(0, "Loading Dictionary");
		
		if (this.devmode) {
			ProjectCompiler comp = new ProjectCompiler();
			
			this.localization = comp.getDictionary(or, this.packages, this.tier);
			
			if (or.hasErrors()) {
				or.exit(104, "Unable to load dictionary file(s)");
				return or;
			}
		}
		else {
			File confdir = new File("./conf");
			this.localization = new Localization();
			
			// TODO make sure that we get in canonical order
			for (File dict : confdir.listFiles(new FilenameFilter() {
				@Override
				public boolean accept(File dir, String name) {					
					return (name.startsWith("dictionary_") && name.endsWith(".xml"));
				}
			})) {
				this.localization.load(or, dict);
			}
			
			if (or.hasErrors()) {
				or.exit(104, "Unable to load dictionary file(s)");
				return or;
			}
		}
		
		or.trace(0, "Dictionary loaded");
		
		return or;
	}
	
	/**
	 * Get a reference to a package's resource file for this Project.
	 *  
	 * @param pkg name of the package the resource resides in
	 * @param filename name of the file, path relative to the pkgresource/[pkg name]/ folder
	 * @return File reference if found, if not error messages in FuncResult
	 */
	public FuncResult<File> getPackageResource(String pkg, String filename) {
		FuncResult<File> res = new FuncResult<File>(false); 
				
		if (this.devmode) {
			for (String rcomponent : this.reversepackages) {
				File f = new File("./packages/" + rcomponent + "/" + this.tier + "/resource/" + pkg + "/" + filename);
				
				if (f.exists()) {
					res.setResult(f);
					break;
				}
				
				f = new File("./packages/" + rcomponent + "/all/resource/" + pkg + "/" + filename);
				
				if (f.exists()) {
					res.setResult(f);
					break;
				}
			}
		}
		else {
			File f = new File("./pkgresource/" + pkg + "/" + filename);
			
			if (f.exists()) 
				res.setResult(f);
		}
		
		if (res.getResult() == null)
			res.errorTr(200, "./pkgresource/" + pkg + "/" + filename);
		
		return res;
	}
	
	/**
	 * Get a reference to a library file (typically a JAR) for this Project.
	 *  
	 * @param filename name of the JAR, path relative to the lib/ folder
	 * @return File reference if found, if not error messages in FuncResult
	 */
	public FuncResult<File> getLibrary(String filename) {
		FuncResult<File> res = new FuncResult<File>(false); 
		
		if (this.devmode) {
			for (String rcomponent : this.reversepackages) {
				File f = new File("./packages/" + rcomponent + "/" + this.tier + "/lib/" + filename);
				
				if (f.exists()) {
					res.setResult(f);
					break;
				}
				
				f = new File("./packages/" + rcomponent + "/all/lib/" + filename);
				
				if (f.exists()) {
					res.setResult(f);
					break;
				}
			}
		}
		else {
			File f = new File("./lib/" + filename);
			
			if (f.exists()) 
				res.setResult(f);
		}
		
		if (res.getResult() == null)
			res.errorTr(200, "./lib/" + filename);
		
		return res;
	}
	
	/**
	 * Get a reference to a resource file specific for this Project.
	 *  
	 * @param filename name of the file, path relative to the projresource/ folder
	 * @return File reference if found, if not error messages in FuncResult
	 */
	public FuncResult<File> getProjectResource(String filename) {
		FuncResult<File> res = new FuncResult<File>(false); 
				
		if (this.devmode) {
			File f = new File("./project/" + this.tier + "/resource/" + filename);
			
			if (f.exists()) 
				res.setResult(f);
			else {
				f = new File("./project/all/resource/" + filename);
				
				if (f.exists()) 
					res.setResult(f);
			}
		}
		else {
			File f = new File("./projresource/" + filename);
			
			if (f.exists()) 
				res.setResult(f);
		}
		
		if (res.getResult() == null)
			res.errorTr(201, "./projresource/" + filename);
		
		return res;
	}
}
