/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 * 
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.aems;

import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import javax.servlet.ServletContext;

import nuts.aems.constant.GC;
import nuts.aems.model.bean.Resource;
import nuts.aems.model.bean.Template;
import nuts.aems.model.bean.User;
import nuts.aems.model.dao.ResourceDAO;
import nuts.aems.model.dao.TemplateDAO;
import nuts.aems.model.example.ResourceExample;
import nuts.aems.model.example.TemplateExample;
import nuts.aems.model.metadata.ResourceMetaData;
import nuts.aems.model.metadata.TemplateMetaData;
import nuts.core.collections.ExpireMap;
import nuts.core.io.IOUtils;
import nuts.core.io.PropertiesEx;
import nuts.core.lang.ClassLoaderUtils;
import nuts.core.lang.ClassUtils;
import nuts.core.lang.MethodUtils;
import nuts.core.orm.dao.DataAccessClient;
import nuts.core.orm.dao.DataAccessClientProxy;
import nuts.core.orm.dao.DataAccessException;
import nuts.core.orm.dao.DataAccessSession;
import nuts.core.orm.dao.DataAccessUtils;
import nuts.core.resource.ExternalResourceLoader;
import nuts.exts.fileupload.UploadManager;
import nuts.exts.freemarker.DatabaseTemplateLoader;
import nuts.exts.freemarker.StaticDelegateTemplateLoader;
import nuts.exts.struts2.interceptor.AuthenticateInterceptor;
import nuts.exts.struts2.mock.StrutsMockSupport;
import nuts.exts.xwork2.util.LocalizedTextUtils;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 */
public class BaseApplet {

	protected static Log log = LogFactory.getLog(BaseApplet.class);

	protected PropertiesEx properties;
	
	protected Map cache;
	
	protected DataAccessClient dataAccessClient;
	
	protected ExternalResourceLoader databaseResourceLoader;
	
	protected DatabaseTemplateLoader databaseTemplateLoader;

	protected ServletContext servletContext;
	
	protected String appVersion;

	protected String adminUsername;
	protected String adminPassword;

	protected static BaseApplet instance;
	
	/**
	 * @return the instance
	 */
	public static BaseApplet get() {
		return instance;
	}

	/**
	 * Constructor
	 * @throws RuntimeException if second instance is to be create
	 */
	public BaseApplet() {
		if (instance != null) {
			throw new RuntimeException(this.getClass().getName() + " must be a singleton instance.");
		}
		instance = this;
	}

	/**
	 * @return the log
	 */
	public Log getLog() {
		return log;
	}

	/**
	 * @return the cache
	 */
	public Map getCache() {
		return cache;
	}

	/**
	 * @param cache the cache to set
	 */
	public void setCache(Map cache) {
		this.cache = cache;
	}

	/**
	 * @return the dataAccessClient
	 */
	public DataAccessClient getDataAccessClient() {
		return dataAccessClient;
	}

	/**
	 * @param dataAccessClient the dataAccessClient to set
	 */
	public void setDataAccessClient(DataAccessClient dataAccessClient) {
		this.dataAccessClient = dataAccessClient;
	}

	/**
	 * @return the databaseResourceLoader
	 */
	public ExternalResourceLoader getDatabaseResourceLoader() {
		return databaseResourceLoader;
	}

	/**
	 * @param databaseResourceLoader the databaseResourceLoader to set
	 */
	public void setDatabaseResourceLoader(ExternalResourceLoader databaseResourceLoader) {
		this.databaseResourceLoader = databaseResourceLoader;
	}

	/**
	 * @return the databaseTemplateLoader
	 */
	public DatabaseTemplateLoader getDatabaseTemplateLoader() {
		return databaseTemplateLoader;
	}

	/**
	 * @param databaseTemplateLoader the databaseTemplateLoader to set
	 */
	public void setDatabaseTemplateLoader(DatabaseTemplateLoader databaseTemplateLoader) {
		this.databaseTemplateLoader = databaseTemplateLoader;
	}

	/**
	 * @return the servletContext
	 */
	public ServletContext getServletContext() {
		return servletContext;
	}

	/**
	 * @param servletContext the servletContext to set
	 */
	public void setServletContext(ServletContext servletContext) {
		this.servletContext = servletContext;
	}

	/**
	 * @return the appVersion
	 */
	public String getAppVersion() {
		return appVersion;
	}

	/**
	 * @param appVersion the appVersion to set
	 */
	public void setAppVersion(String appVersion) {
		this.appVersion = appVersion;
	}

	/**
	 * @return the adminUsername
	 */
	public String getAdminUsername() {
		return adminUsername;
	}

	/**
	 * @param adminUsername the adminUsername to set
	 */
	public void setAdminUsername(String adminUsername) {
		this.adminUsername = adminUsername;
	}

	/**
	 * @return the adminPassword
	 */
	public String getAdminPassword() {
		return adminPassword;
	}

	/**
	 * @param adminPassword the adminPassword to set
	 */
	public void setAdminPassword(String adminPassword) {
		this.adminPassword = adminPassword;
	}

	/**
	 * @return the properties
	 */
	public PropertiesEx getProperties() {
		return properties;
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public boolean getPropertyAsBoolean(String key) {
		return properties.getPropertyAsBoolean(key);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public boolean getPropertyAsBoolean(String key, boolean defv) {
		return properties.getPropertyAsBoolean(key, defv);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public int getPropertyAsInt(String key) {
		return properties.getPropertyAsInt(key);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public int getPropertyAsInt(String key, int defaultVar) {
		return properties.getPropertyAsInt(key, defaultVar);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public long getPropertyAsLong(String key) {
		return properties.getPropertyAsLong(key);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public long getPropertyAsLong(String key, long defaultVar) {
		return properties.getPropertyAsLong(key, defaultVar);
	}

	/**
	 * @param name resource name
	 * @return List value
	 */
	public List getPropertyAsList(String name) {
		return properties.getPropertyAsList(name);
	}
	
	/**
	 * @param name resource name
	 * @param defaultValue default value
	 * @return list value
	 */
	public List getPropertyAsList(String name, List defaultValue) {
		return properties.getPropertyAsList(name, defaultValue);
	}

	/**
	 * @param name resource name
	 * @return map value
	 */
	public Map getPropertyAsMap(String name) {
		return properties.getPropertyAsMap(name);
	}
	
	/**
	 * @param name resource name
	 * @param defaultValue default value
	 * @return map value
	 */
	public Map getPropertyAsMap(String name, Map defaultValue) {
		return properties.getPropertyAsMap(name, defaultValue);
	}

	/**
	 * @param key key
	 * @return the property
	 */
	public String getProperty(String key) {
		return properties.getProperty(key);
	}

	/**
	 * @param key key
	 * @param def default value
	 * @return the property
	 */
	public String getProperty(String key, String def) {
		return properties.getProperty(key, def);
	}

	public String getPropertyAsPath(String name) {
		return getPropertyAsPath(name, null);
	}

	public String getPropertyAsPath(String name, String defv) {
		String dir = getProperty(name, defv);
		if (dir != null && dir.startsWith("~")) {
			dir = getServletContext().getRealPath(dir.substring(1));
		}
		return dir;
	}

	public User createAdminUser() {
		User user = new User();
		user.setId(0L);
		user.setName(getAdminUsername());
		user.setEmail(getAdminUsername());
		user.setPassword(getAdminPassword());
		user.setGroupLevel(GC.ADMIN_LEVEL);
		user.setGroupPermits(Arrays.asList(new String[] { GC.PERMISSION_ALL }));
		return user;
	}


	/**
	 * @param username username
	 * @param password password
	 * @return true if username & password equals properties setting
	 */
	public boolean isAdminUser(String username, String password) {
		return (getAdminUsername().equals(username) 
				&& getAdminPassword().equals(password));
	}
	
	/**
	 * @return the dataAccessSession
	 * @throws DataAccessException if an error occur
	 */
	public DataAccessSession openDataAccessSession() throws DataAccessException {
		return dataAccessClient.openSession();
	}

	/**
	 * destroy
	 */
	public synchronized void destroy() {
		log.info("Application (" + servletContext.getContextPath() + ") destroying...");
		
		shutdownCache();
		
		log.info("Application (" + servletContext.getContextPath() + ") destroyed.");
	}

	/**
	 * initialize
	 * @param contextRoot servlet context root path
	 * @param initStruts init struts
	 */
	public synchronized void initialize(String contextRoot, boolean initStruts) {
		if (initStruts) {
			StrutsMockSupport.initDispatcher(contextRoot);
		}
		else {
			StrutsMockSupport.initContext(contextRoot);
		}
		initialize(StrutsMockSupport.getMockServletContext());
	}
	
	/**
	 * initialize
	 * @param servletContext ServletContext
	 */
	public synchronized void initialize(ServletContext servletContext) {
		log.info("Application (" + servletContext.getContextPath() + ") initializing ...");

		try {
			this.servletContext = servletContext;

			loadSystemProps();

			initUploadSetting();
			
			buildCache();
			
			buildDataAccessClient();
			
			initialize();
			
			startDaemonCronTask();
		}
		catch (Exception ex) {
			log.error("Application (" + servletContext.getContextPath() + ") initialize failed!", ex);
			throw new RuntimeException(ex);
		}

		log.info("Application (" + servletContext.getContextPath() + ") initialized.");
	}

	protected void loadProperties(String res) throws Exception {
		InputStream is = null;
		try {
			is = ClassLoaderUtils.getResourceAsStream(res);
			properties.load(is);
		}
		finally {
			IOUtils.closeQuietly(is);
		}
	}

	protected void loadSystemProps() throws Exception {
		properties = new PropertiesEx();
		loadProperties("system.properties");
		loadProperties("nuts-dao.properties");
		properties.putAll(System.getProperties());
		if ("true".equals(System.getenv("nuts.env"))) {
			properties.putAll(System.getenv());
		}
		
		try {
			loadProperties("test.properties");
		}
		catch (Exception e) {
			
		}

		appVersion = getProperty("prj.version") + "." 
			+ getProperty("prj.revision");

		log.info("Version: " + appVersion);

		adminUsername = getProperty("admin-username");
		adminPassword = getProperty("admin-password");
	}

	protected String normalizePath(String path) {
		path.replace('\\', '/');
		if (!path.endsWith("/")) {
			path += "/";
		}
		return path;
	}

	protected void buildCache() throws Exception {
		String cn = getProperty("cache.name", "");
		int ce = getPropertyAsInt("cache.expire", 0);

		Class ch = null;
		try {
			ch = ClassUtils.getClass(BaseApplet.class.getPackage().getName() + ".EHCacheHelper");
		}
		catch (Throwable e) {
			log.info("Failed to load EHCache - " + e.getMessage());
		}

		if (ch != null) {
			cache = (Map)MethodUtils.invokeStaticMethod(ch, "buildJCache", 
				new Object[] { cn, ce });
		}
		if (cache == null) {
			if (ce > 0) {
				cache = Collections.synchronizedMap(new ExpireMap<String, String>(
						new WeakHashMap<String, String>(), ce * 1000));
			}
			else {
				cache = Collections.synchronizedMap(new WeakHashMap<String, String>());
			}
		}
	}
	
	protected void shutdownCache() {
		Class ch = null;
		try {
			ch = ClassUtils.getClass(BaseApplet.class.getPackage().getName() + ".EHCacheHelper");
		}
		catch (Throwable e) {
			log.info("Failed to load EHCache - " + e.getMessage());
		}

		if (ch != null) {
			try {
				MethodUtils.invokeStaticMethod(ch, "shutdownCache");
			}
			catch (Exception e) {
				log.error("Failed to shutdown EHCache", e);
			}
		}
	}
	
	protected void buildDataAccessClient () throws Exception {
		dataAccessClient = MyBatisHelper.buildDataAccessClient();

		DataAccessClientProxy.setDataAccessClient(dataAccessClient);
	}

	protected void initUploadSetting() throws Exception {
		UploadManager.setSaveDir(getPropertyAsPath("upload.directory", "/WEB-INF/upload"));
	}

	protected void initActionPermits() throws Exception {
		AuthenticateInterceptor.loadActionPermits();
	}
	
	/**
	 * @throws Exception if an error occurs
	 */
	protected void initDatabaseResourcesLoader() throws Exception {
		if (getPropertyAsBoolean("resource.database", true)) {
			databaseResourceLoader = ExternalResourceLoader.getInstance();

			databaseResourceLoader.setClassColumn(ResourceMetaData.PN_CLAZZ);
			databaseResourceLoader.setLanguageColumn(ResourceMetaData.PN_LANGUAGE);
			databaseResourceLoader.setCountryColumn(ResourceMetaData.PN_COUNTRY);
			//databaseResourceLoader.setVariantColumn("VARIANT");
			databaseResourceLoader.setNameColumn(ResourceMetaData.PN_NAME);
			databaseResourceLoader.setValueColumn(ResourceMetaData.PN_VALUE);
			databaseResourceLoader.setPackageName(this.getClass().getPackage().getName());

			DataAccessSession das = getDataAccessClient().openSession();
			try {
				ResourceDAO tDao = new ResourceDAO(das);
				ResourceExample tExp = tDao.createExample();
				tExp.invalid().isFalse();
				
				List<Resource> list = tDao.selectByExample(tExp);
				databaseResourceLoader.loadResources(list);
			}
			finally {
				DataAccessUtils.closeQuietly(das);
			}

			LocalizedTextUtils.setDelegatedClassLoader(databaseResourceLoader.getClassLoader());
		}
	}

	/**
	 * @throws Exception if an error occurs
	 */
	protected void initDatabaseTemplateLoader() throws Exception {
		databaseTemplateLoader = new DatabaseTemplateLoader();
		
		databaseTemplateLoader.setNameColumn(TemplateMetaData.PN_NAME);
		databaseTemplateLoader.setLanguageColumn(TemplateMetaData.PN_LANGUAGE);
		databaseTemplateLoader.setCountryColumn(TemplateMetaData.PN_COUNTRY);
		//databaseTemplateLoader.setVariantColumn("VARIANT");
		databaseTemplateLoader.setSourceColumn(TemplateMetaData.PN_SOURCE);
		databaseTemplateLoader.setTimestampColumn(TemplateMetaData.PN_UTIME);

		DataAccessSession das = getDataAccessClient().openSession();
		try {
			TemplateDAO tDao = new TemplateDAO(das);
			TemplateExample tExp = tDao.createExample();
			tExp.invalid().isFalse();
			
			List<Template> list = tDao.selectByExample(tExp);
			databaseTemplateLoader.loadTemplates(list);
		}
		finally {
			DataAccessUtils.closeQuietly(das);
		}

		StaticDelegateTemplateLoader.setDelegate(databaseTemplateLoader);
	}

	public void onResourceLoaded() throws Exception {
		
	}
	
	protected void startDaemonCronTask() throws Exception {
		//TODO
		if ("true".equals(properties.getProperty("daemon.cron"))) {
			log.info("Starting daemon cron task ...");
		}
	}
	
	protected void initialize() throws Exception {
		
	}

	//-----------------------------------------------
	// Google App Engine
	//-----------------------------------------------
	private static boolean gaeSupport;
	
	static {
		try {
			ClassUtils.getClass("nuts.gae.vfs.ngfs.provider.GaeVFS");
			gaeSupport = true;
		}
		catch (Throwable e) {
			gaeSupport = false;
		}
	}

	/**
	 * @return true if gae support
	 */
	public static boolean isGaeSupport() {
		return gaeSupport;
	}
}
