// Copyright (c) 2003-present, Jodd Team (http://jodd.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:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. 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.
//
// 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 jodd.joy.core;

import jodd.db.DbManager;
import jodd.db.DbSessionProvider;
import jodd.db.connection.ConnectionProvider;
import jodd.db.oom.DbOomManager;
import jodd.db.oom.config.AutomagicDbOomConfigurator;
import jodd.db.pool.CoreConnectionPool;
import jodd.joy.exception.AppException;
import jodd.jtx.meta.ReadWriteTransaction;
import jodd.jtx.JtxTransactionManager;
import jodd.db.jtx.DbJtxSessionProvider;
import jodd.db.jtx.DbJtxTransactionManager;
import jodd.jtx.meta.Transaction;
import jodd.jtx.proxy.AnnotationTxAdvice;
import jodd.jtx.proxy.AnnotationTxAdviceManager;
import jodd.jtx.proxy.AnnotationTxAdviceSupport;
import jodd.petite.PetiteContainer;
import jodd.petite.config.AutomagicPetiteConfigurator;
import jodd.petite.proxetta.ProxettaAwarePetiteContainer;
import jodd.petite.scope.SessionScope;
import jodd.petite.scope.SingletonScope;
import jodd.props.Props;
import jodd.props.PropsUtil;
import jodd.proxetta.MethodInfo;
import jodd.proxetta.ProxyAspect;
import jodd.proxetta.impl.ProxyProxetta;
import jodd.proxetta.pointcuts.MethodAnnotationPointcut;
import jodd.util.ClassLoaderUtil;
import jodd.util.SystemUtil;
import jodd.log.Logger;
import jodd.log.LoggerFactory;

import java.lang.annotation.Annotation;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;

/**
 * Default application core. Contains init points to
 * all application frameworks and layers.
 */
public abstract class DefaultAppCore {

	/**
	 * Application system property - application folder.
	 */
	public static final String APP_DIR = "app.dir";
	/**
	 * Application system property - flag if web application is detected..
	 */
	public static final String APP_WEB = "app.web";

	/**
	 * Petite bean name for AppCore (this instance).
	 */
	public static final String PETITE_CORE = "core";
	/**
	 * Petite bean name for database pool.
	 */
	public static final String PETITE_DBPOOL = "dbpool";
	/**
	 * Petite bean name for <code>DbManager</code> instance.
	 */
	public static final String PETITE_DB = "db";
	/**
	 * Petite bean name for <code>DbOomManager</code> instance.
	 */
	public static final String PETITE_DBOOM = "dboom";
	/**
	 * Petite bean name for {@link AppInit} bean.
	 */
	public static final String PETITE_INIT = "init";
	/**
	 * Petite bean name for application props.
	 */
	public static final String PETITE_PROPS = "props";
	/**
	 * Petite bean name for {@link AppScanner} bean.
	 */
	public static final String PETITE_SCAN = "scan";


	/**
	 * Logger. Resolved during {@link #initLogger() initialization}.
	 */
	protected static Logger log;

	/**
	 * App dir. Resolved during initialization.
	 */
	protected String appDir;

	/**
	 * Is web application. Resolved during initialization.
	 */
	protected boolean isWebApplication;

	/**
	 * Props profiles. If set, overrides any default profile set
	 * in the props files.
	 */
	protected String[] appPropsProfiles;

	/**
	 * Default constructor.
	 */
	protected DefaultAppCore() {
	}

	// ---------------------------------------------------------------- init
	
	/**
	 * Returns <code>true</code> if application is started as a part of web application.
	 */
	public boolean isWebApplication() {
		return isWebApplication;
	}

	/**
	 * Returns application directory.
	 */
	public String getAppDir() {
		return appDir;
	}

	/**
	 * Initializes application core, invoked very first!
	 * Important: logging is not yet available in this method!
	 */
	@SuppressWarnings("unchecked")
	public void initCore() {
		if (appPropsName == null) {
			appPropsName = "app.props";
		}

		if (appPropsNamePattern == null) {
			int index = appPropsName.lastIndexOf('.');

			appPropsNamePattern = '/' + appPropsName.substring(0, index) + "*.prop*";
		}

		if (jtxAnnotations == null) {
			jtxAnnotations = new Class[] {Transaction.class, ReadWriteTransaction.class};
		}

		if (jtxScopePattern == null) {
			jtxScopePattern = "$class";
		}

		if (appDir == null) {
			resolveAppDir(appPropsName);		// app directory is resolved from location of 'app.props'.
		}

		System.setProperty(APP_DIR, appDir);
		System.setProperty(APP_WEB, Boolean.toString(isWebApplication));
	}

	/**
	 * Initializes the logger, after the log path is {@link #init() defined}.
	 */
	protected void initLogger() {
		if (log != null) {
			return;
		}

		log = LoggerFactory.getLogger(DefaultAppCore.class);
		log.info("app dir: " + appDir);
	}

	/**
	 * Resolves application root folders.
	 * <p>
	 * If application is started as web application, app folder is one below the WEB-INF folder.
	 * Otherwise, the root folder is equal to the working folder.
	 */
	protected void resolveAppDir(String classPathFileName) {
		URL url = ClassLoaderUtil.getResourceUrl(classPathFileName);
		if (url == null) {
			throw new AppException("Failed to resolve app dir, missing: " + classPathFileName);
		}
		String protocol = url.getProtocol();


		if (!protocol.equals("file")) {
			try {
				url = new URL(url.getFile());
			} catch (MalformedURLException ignore) {
			}
		}

		appDir = url.getFile();

		int ndx = appDir.indexOf("WEB-INF");
		isWebApplication = (ndx != -1);

		appDir = isWebApplication ? appDir.substring(0, ndx) : SystemUtil.workingFolder();
	}

	// ---------------------------------------------------------------- ready

	/**
	 * Called after the {@link #init() core initialization},
	 * during the {@link #start() application startup}.
	 */
	protected void ready() {
	}

	// ---------------------------------------------------------------- start

	protected boolean initialized;

	/**
	 * Initializes application.
	 * May be called several times, but the core
	 * will be initialized just once.
	 * Usually called manually when core needs to
	 * be created before server is started
	 * (e.g. in embedded environments)
	 */
	public void init() {
		if (initialized) {
			return;
		}

		initCore();
		initLogger();
		initProps();
		initScanner();

		initialized = true;
	}


	/**
	 * Starts the application and performs all initialization.
	 */
	public void start() {
		init();

		ready();

		try {
			startProxetta();
			startPetite();
			startDb();
			startApp();

			log.info("app started");
		} catch (RuntimeException rex) {
			if (log != null) {
				log.error(rex.toString(), rex);
			} else {
				System.out.println(rex.toString());
				rex.printStackTrace();
			}
			try {
				stop();
			} catch (Exception ignore) {
			}
			throw rex;
		}
	}

	/**
	 * Stops the application.
	 */
	public void stop() {
		if (log != null) {
			log.info("shutting down...");
		}

		stopApp();
		stopDb();
		stopPetite();

		if (log != null) {
			log.info("app stopped");
		}
	}

	// ---------------------------------------------------------------- props

	/**
	 * Main application props file name, must exist in class path.
	 */
	protected String appPropsName;

	/**
	 * Application props file name pattern.
	 */
	protected String appPropsNamePattern;

	/**
	 * Application props.
	 */
	protected Props appProps;

	/**
	 * Returns applications properties loaded from props files.
	 */
	public Props getAppProps() {
		return appProps;
	}

	/**
	 * Creates and loads application props.
	 * It first loads system properties (registered as <code>sys.*</code>)
	 * and then environment properties (registered as <code>env.*</code>).
	 * Finally, props files are read from the classpath. All properties
	 * are loaded using
	 * <p>
	 * If props have been already loaded, does nothing.
	 */
	protected void initProps() {
		if (appProps != null) {
			return;
		}

		appProps = createProps();

		appProps.loadSystemProperties("sys");
		appProps.loadEnvironment("env");

		PropsUtil.loadFromClasspath(appProps, appPropsNamePattern);

		if (appPropsProfiles != null) {
			appProps.setActiveProfiles(appPropsProfiles);
		}
	}

	/**
	 * Creates new Props. Empty props will be ignored,
	 * and missing macros will be resolved as empty string.
	 */
	protected Props createProps() {
		Props props = new Props();
		props.setSkipEmptyProps(true);
		props.setIgnoreMissingMacros(true);
		return props;
	}

	// ---------------------------------------------------------------- scanning

	protected AppScanner appScanner;

	/**
	 * Returns scanner.
	 */
	public AppScanner getAppScanner() {
		return appScanner;
	}

	/**
	 * Initializes {@link AppScanner}.
	 */
	protected void initScanner() {
		if (appScanner != null) {
			return;
		}

		appScanner = new AppScanner(this);
	}

	// ---------------------------------------------------------------- proxetta

	protected ProxyProxetta proxetta;

	/**
	 * Returns proxetta.
	 */
	public ProxyProxetta getProxetta() {
		return proxetta;
	}

	/**
	 * Creates Proxetta with all aspects. The following aspects are created:
	 * <ul>
	 * <li>Transaction proxy - applied on all classes that contains public top-level methods
	 * annotated with <code>@Transaction</code> annotation. This is just one way how proxies
	 * can be applied - since base configuration is in Java, everything is possible.</li>
	 * </ul>
	 */
	protected void startProxetta() {
		log.info("proxetta initialization");
		proxetta = ProxyProxetta.withAspects(createAppAspects());
	}

	/**
	 * Creates all application aspects. By default it creates just
	 * {@link #createTxProxyAspects() transactional aspect}.
	 */
	protected ProxyAspect[] createAppAspects() {
		return new ProxyAspect[] {createTxProxyAspects()};
	}

	/**
	 * Creates TX aspect that will be applied on all classes
	 * having at least one public top-level method annotated
	 * with {@link #jtxAnnotations registered JTX annotations}.
	 */
	protected ProxyAspect createTxProxyAspects() {
		return new ProxyAspect(
				AnnotationTxAdvice.class,
				new MethodAnnotationPointcut(jtxAnnotations) {
			@Override
			public boolean apply(MethodInfo methodInfo) {
				return
						isPublic(methodInfo) &&
						isTopLevelMethod(methodInfo) &&
						super.apply(methodInfo);
			}
		});
	}

	// ---------------------------------------------------------------- petite

	protected PetiteContainer petite;

	/**
	 * Returns application container (Petite).
	 */
	public PetiteContainer getPetite() {
		return petite;
	}


	/**
	 * Creates and initializes Petite container.
	 * It will be auto-magically configured by scanning the classpath.
	 * Also, all 'app*.prop*' will be loaded and values will
	 * be injected in the matched beans. At the end it registers
	 * this instance of core into the container.
	 */
	protected void startPetite() {
		log.info("petite initialization");
		petite = createPetiteContainer();

		log.info("app in web: " + Boolean.valueOf(isWebApplication));
		if (!isWebApplication) {
			// make session scope to act as singleton scope
			// if this is not a web application (and http session is not available).
			petite.registerScope(SessionScope.class, new SingletonScope());
		}

		// load parameters from properties files
		petite.defineParameters(appProps);

		// adds a scanner bean, so it can be immediately configured from props
		petite.addBean(PETITE_SCAN, appScanner);

		// automagic configuration
		registerPetiteContainerBeans(petite);

		// add AppCore instance to Petite
		petite.addBean(PETITE_CORE, this);

		petite.addBean(PETITE_PROPS, appProps);
	}

	/**
	 * Configures Petite container. By default scans the class path
	 * for petite beans and registers them automagically.
	 */
	protected void registerPetiteContainerBeans(PetiteContainer petiteContainer) {
		AutomagicPetiteConfigurator pcfg = new AutomagicPetiteConfigurator();
		appScanner.configure(pcfg);
		pcfg.configure(petiteContainer);
	}

	/**
	 * Creates Petite container. By default, it creates
	 * {@link jodd.petite.proxetta.ProxettaAwarePetiteContainer proxetta aware petite container}.
	 */
	protected PetiteContainer createPetiteContainer() {
		return new ProxettaAwarePetiteContainer(proxetta);
	}

	/**
	 * Stops Petite container.
	 */
	protected void stopPetite() {
		if (petite != null) {
			petite.shutdown();
		}
	}

	// ---------------------------------------------------------------- database

	protected boolean useDatabase = true;

	/**
	 * JTX manager.
	 */
	protected JtxTransactionManager jtxManager;

	/**
	 * Returns JTX transaction manager.
	 */
	public JtxTransactionManager getJtxManager() {
		return jtxManager;
	}

	/**
	 * Database connection provider.
	 */
	protected ConnectionProvider connectionProvider;

	/**
	 * JTX annotations.
	 */
	protected Class<? extends Annotation>[] jtxAnnotations;

	/**
	 * JTX scope pattern.
	 * @see AnnotationTxAdviceManager
	 */
	protected String jtxScopePattern;

	/**
	 * Initializes database. First, creates connection pool.
	 * and transaction manager. Then, Jodds DbOomManager is
	 * configured. It is also configured automagically, by scanning
	 * the class path for entities.
	 */
	@SuppressWarnings("unchecked")
	protected void startDb() {
		if (!useDatabase) {
			log.info("database is not used");
			return;
		}

		log.info("database initialization");

		// connection pool
		connectionProvider = createConnectionProvider();
		petite.addBean(PETITE_DBPOOL, connectionProvider);
		connectionProvider.init();

		checkConnectionProvider();

		// transactions manager
		jtxManager = createJtxTransactionManager(connectionProvider);
		jtxManager.setValidateExistingTransaction(true);

		AnnotationTxAdviceManager annTxAdviceManager = new AnnotationTxAdviceManager(jtxManager, jtxScopePattern);
		annTxAdviceManager.registerAnnotations(jtxAnnotations);
		AnnotationTxAdviceSupport.manager = annTxAdviceManager;

		DbSessionProvider sessionProvider = new DbJtxSessionProvider(jtxManager);

		// global settings
		DbManager dbManager = DbManager.getInstance();
		dbManager.setConnectionProvider(connectionProvider);
		dbManager.setSessionProvider(sessionProvider);
		petite.addBean(PETITE_DB, dbManager);

		DbOomManager dbOomManager = DbOomManager.getInstance();
		petite.addBean(PETITE_DBOOM, dbOomManager);

		// automatic configuration
		registerDbEntities(dbOomManager);
	}

	/**
	 * Registers DbOom entities. By default, scans the
	 * class path and register entities automagically.
	 */
	protected void registerDbEntities(DbOomManager dbOomManager) {
		AutomagicDbOomConfigurator dbcfg = new AutomagicDbOomConfigurator();
		appScanner.configure(dbcfg);
		dbcfg.configure(dbOomManager);
	}

	/**
	 * Creates JTX transaction manager.
	 */
	protected JtxTransactionManager createJtxTransactionManager(ConnectionProvider connectionProvider) {
		return new DbJtxTransactionManager(connectionProvider);
	}

	/**
	 * Returns <code>ConnectionProvider</code> instance.
	 * Instance will be registered into the Petite context.
	 */
	protected ConnectionProvider createConnectionProvider() {
		return new CoreConnectionPool();
	}

	/**
	 * Checks if connection provider can return a connection.
	 */
	protected void checkConnectionProvider() {
		Connection connection = connectionProvider.getConnection();
		try {
			DatabaseMetaData databaseMetaData = connection.getMetaData();
			String name = databaseMetaData.getDatabaseProductName();
			String version = databaseMetaData.getDatabaseProductVersion();

			if (log.isInfoEnabled()) {
				log.info("Connected to database: " + name + " v" + version);
			}

		} catch (SQLException sex) {
			log.error("DB connection failed: ", sex);
		} finally {
			connectionProvider.closeConnection(connection);
		}
	}

	/**
	 * Closes database resources at the end.
	 */
	protected void stopDb() {
		if (!useDatabase) {
			return;
		}

		if (log != null) {
			log.info("database shutdown");
		}

		if (jtxManager != null) {
			jtxManager.close();
		}

		if (connectionProvider != null) {
			connectionProvider.close();
		}
	}

	// ---------------------------------------------------------------- init

	protected AppInit appInit;

	/**
	 * Initializes business part of the application.
	 * Simply delegates to {@link AppInit#init()}.
	 */
	protected void startApp() {
		appInit = petite.getBean(PETITE_INIT);
		if (appInit != null) {
			appInit.init();
		}
	}

	/**
	 * Stops business part of the application.
	 * Simply delegates to {@link AppInit#stop()}.
	 */
	protected void stopApp() {
		if (appInit != null) {
			appInit.stop();
		}
	}

}