/*
 * Copyright 2012-2017 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.jta.atomikos;

import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Bean friendly variant of
 * <a href="http://www.atomikos.com/Documentation/JtaProperties">Atomikos configuration
 * properties</a>. Allows for setter based configuration and is amiable to relaxed data
 * binding.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 1.2.0
 * @see #asProperties()
 */
@ConfigurationProperties(prefix = "spring.jta.atomikos.properties")
public class AtomikosProperties {

	private final Map<String, String> values = new TreeMap<>();

	/**
	 * Transaction manager implementation that should be started.
	 */
	private String service;

	/**
	 * Maximum timeout (in milliseconds) that can be allowed for transactions.
	 */
	private long maxTimeout = 300000;

	/**
	 * Default timeout for JTA transactions.
	 */
	private long defaultJtaTimeout = 10000;

	/**
	 * Maximum number of active transactions.
	 */
	private int maxActives = 50;

	/**
	 * Enable disk logging.
	 */
	private boolean enableLogging = true;

	/**
	 * Transaction manager's unique name. Defaults to the machine's IP address. If you
	 * plan to run more than one transaction manager against one database you must set
	 * this property to a unique value.
	 */
	private String transactionManagerUniqueName;

	/**
	 * Specify if sub-transactions should be joined when possible.
	 */
	private boolean serialJtaTransactions = true;

	/**
	 * Specify if a VM shutdown should trigger forced shutdown of the transaction core.
	 */
	private boolean forceShutdownOnVmExit;

	/**
	 * Transactions log file base name.
	 */
	private String logBaseName = "tmlog";

	/**
	 * Directory in which the log files should be stored. Defaults to the current working
	 * directory.
	 */
	private String logBaseDir;

	/**
	 * Interval between checkpoints. A checkpoint reduces the log file size at the expense
	 * of adding some overhead in the runtime.
	 */
	private long checkpointInterval = 500;

	/**
	 * Console log level.
	 */
	private AtomikosLoggingLevel consoleLogLevel = AtomikosLoggingLevel.WARN;

	/**
	 * Directory in which to store the debug log files. Defaults to the current working
	 * directory.
	 */
	private String outputDir;

	/**
	 * Debug logs file name.
	 */
	private String consoleFileName = "tm.out";

	/**
	 * Number of debug logs files that can be created.
	 */
	private int consoleFileCount = 1;

	/**
	 * How many bytes can be stored at most in debug logs files. Negative values means
	 * unlimited.
	 */
	private int consoleFileLimit = -1;

	/**
	 * Use different (and concurrent) threads for two-phase commit on the participating
	 * resources.
	 */
	private boolean threadedTwoPhaseCommit = true;

	/**
	 * Specifies the transaction manager implementation that should be started. There is
	 * no default value and this must be set. Generally,
	 * {@literal com.atomikos.icatch.standalone.UserTransactionServiceFactory} is the
	 * value you should set.
	 * @param service the service
	 */
	public void setService(String service) {
		this.service = service;
		set("service", service);
	}

	public String getService() {
		return this.service;
	}

	/**
	 * Specifies the maximum timeout (in milliseconds) that can be allowed for
	 * transactions. Defaults to {@literal 300000}. This means that calls to
	 * UserTransaction.setTransactionTimeout() with a value higher than configured here
	 * will be max'ed to this value.
	 * @param maxTimeout the max timeout
	 */
	public void setMaxTimeout(long maxTimeout) {
		this.maxTimeout = maxTimeout;
		set("max_timeout", maxTimeout);
	}

	public long getMaxTimeout() {
		return this.maxTimeout;
	}

	/**
	 * The default timeout for JTA transactions (optional, defaults to {@literal 10000}
	 * ms).
	 * @param defaultJtaTimeout the default JTA timeout
	 */
	public void setDefaultJtaTimeout(long defaultJtaTimeout) {
		this.defaultJtaTimeout = defaultJtaTimeout;
		set("default_jta_timeout", defaultJtaTimeout);
	}

	public long getDefaultJtaTimeout() {
		return this.defaultJtaTimeout;
	}

	/**
	 * Specifies the maximum number of active transactions. Defaults to {@literal 50}. A
	 * negative value means infinite amount. You will get an {@code IllegalStateException}
	 * with error message "Max number of active transactions reached" if you call
	 * {@code UserTransaction.begin()} while there are already n concurrent transactions
	 * running, n being this value.
	 * @param maxActives the max activities
	 */
	public void setMaxActives(int maxActives) {
		this.maxActives = maxActives;
		set("max_actives", maxActives);
	}

	public int getMaxActives() {
		return this.maxActives;
	}

	/**
	 * Specifies if disk logging should be enabled or not. Defaults to true. It is useful
	 * for JUnit testing, or to profile code without seeing the transaction manager's
	 * activity as a hot spot but this should never be disabled on production or data
	 * integrity cannot be guaranteed.
	 * @param enableLogging if logging is enabled
	 */
	public void setEnableLogging(boolean enableLogging) {
		this.enableLogging = enableLogging;
		set("enable_logging", enableLogging);
	}

	public boolean isEnableLogging() {
		return this.enableLogging;
	}

	/**
	 * Specifies the transaction manager's unique name. Defaults to the machine's IP
	 * address. If you plan to run more than one transaction manager against one database
	 * you must set this property to a unique value or you might run into duplicate
	 * transaction ID (XID) problems that can be quite subtle (example:
	 * {@literal http://fogbugz.atomikos.com/default.asp?community.6.2225.7}). If multiple
	 * instances need to use the same properties file then the easiest way to ensure
	 * uniqueness for this property is by referencing a system property specified at VM
	 * startup.
	 * @param uniqueName the unique name
	 */
	public void setTransactionManagerUniqueName(String uniqueName) {
		this.transactionManagerUniqueName = uniqueName;
		set("tm_unique_name", uniqueName);
	}

	public String getTransactionManagerUniqueName() {
		return this.transactionManagerUniqueName;
	}

	/**
	 * Specifies if subtransactions should be joined when possible. Defaults to true. When
	 * false, no attempt to call {@code XAResource.start(TM_JOIN)} will be made for
	 * different but related subtransactions. This setting has no effect on resource
	 * access within one and the same transaction. If you don't use subtransactions then
	 * this setting can be ignored.
	 * @param serialJtaTransactions if serial JTA transaction are supported
	 */
	public void setSerialJtaTransactions(boolean serialJtaTransactions) {
		this.serialJtaTransactions = serialJtaTransactions;
		set("serial_jta_transactions", serialJtaTransactions);
	}

	public boolean isSerialJtaTransactions() {
		return this.serialJtaTransactions;
	}

	/**
	 * Specifies whether VM shutdown should trigger forced shutdown of the transaction
	 * core. Defaults to false.
	 * @param forceShutdownOnVmExit if VM shutdown should be forced
	 */
	public void setForceShutdownOnVmExit(boolean forceShutdownOnVmExit) {
		this.forceShutdownOnVmExit = forceShutdownOnVmExit;
		set("force_shutdown_on_vm_exit", forceShutdownOnVmExit);
	}

	public boolean isForceShutdownOnVmExit() {
		return this.forceShutdownOnVmExit;
	}

	/**
	 * Specifies the transactions log file base name. Defaults to {@literal tmlog}. The
	 * transactions logs are stored in files using this name appended with a number and
	 * the extension {@literal .log}. At checkpoint, a new transactions log file is
	 * created and the number is incremented.
	 * @param logBaseName the log base name
	 */
	public void setLogBaseName(String logBaseName) {
		this.logBaseName = logBaseName;
		set("log_base_name", logBaseName);
	}

	public String getLogBaseName() {
		return this.logBaseName;
	}

	/**
	 * Specifies the directory in which the log files should be stored. Defaults to the
	 * current working directory. This directory should be a stable storage like a SAN,
	 * RAID or at least backed up location. The transactions logs files are as important
	 * as the data themselves to guarantee consistency in case of failures.
	 * @param logBaseDir the log base dir
	 */
	public void setLogBaseDir(String logBaseDir) {
		this.logBaseDir = logBaseDir;
		set("log_base_dir", logBaseDir);
	}

	public String getLogBaseDir() {
		return this.logBaseDir;
	}

	/**
	 * Specifies the interval between checkpoints. A checkpoint reduces the log file size
	 * at the expense of adding some overhead in the runtime. Defaults to {@literal 500}.
	 * @param checkpointInterval the checkpoint interval
	 */
	public void setCheckpointInterval(long checkpointInterval) {
		this.checkpointInterval = checkpointInterval;
		set("checkpoint_interval", checkpointInterval);
	}

	public long getCheckpointInterval() {
		return this.checkpointInterval;
	}

	/**
	 * Specifies the console log level. Defaults to {@link AtomikosLoggingLevel#WARN}.
	 * @param consoleLogLevel the console log level
	 */
	public void setConsoleLogLevel(AtomikosLoggingLevel consoleLogLevel) {
		this.consoleLogLevel = consoleLogLevel;
		set("console_log_level", consoleLogLevel);
	}

	public AtomikosLoggingLevel getConsoleLogLevel() {
		return this.consoleLogLevel;
	}

	/**
	 * Specifies the directory in which to store the debug log files. Defaults to the
	 * current working directory.
	 * @param outputDir the output dir
	 */
	public void setOutputDir(String outputDir) {
		this.outputDir = outputDir;
		set("output_dir", outputDir);
	}

	public String getOutputDir() {
		return this.outputDir;
	}

	/**
	 * Specifies the debug logs file name. Defaults to {@literal tm.out}.
	 * @param consoleFileName the console file name
	 */
	public void setConsoleFileName(String consoleFileName) {
		this.consoleFileName = consoleFileName;
		set("console_file_name", consoleFileName);
	}

	public String getConsoleFileName() {
		return this.consoleFileName;
	}

	/**
	 * Specifies how many debug logs files can be created. Defaults to {@literal 1}.
	 * @param consoleFileCount the console file count
	 */
	public void setConsoleFileCount(int consoleFileCount) {
		this.consoleFileCount = consoleFileCount;
		set("console_file_count", consoleFileCount);
	}

	public int getConsoleFileCount() {
		return this.consoleFileCount;
	}

	/**
	 * Specifies how many bytes can be stored at most in debug logs files. Defaults to
	 * {@literal -1}. Negative values means unlimited.
	 * @param consoleFileLimit the console file limit
	 */
	public void setConsoleFileLimit(int consoleFileLimit) {
		this.consoleFileLimit = consoleFileLimit;
		set("console_file_limit", consoleFileLimit);
	}

	public int getConsoleFileLimit() {
		return this.consoleFileLimit;
	}

	/**
	 * Specifies whether or not to use different (and concurrent) threads for two-phase
	 * commit on the participating resources. Setting this to {@literal true} implies that
	 * the commit is more efficient since waiting for acknowledgements is done in
	 * parallel. Defaults to {@literal true}. If you set this to {@literal false}, then
	 * commits will happen in the order that resources are accessed within the
	 * transaction.
	 * @param threadedTwoPhaseCommit if threaded two phase commits should be used
	 */
	public void setThreadedTwoPhaseCommit(boolean threadedTwoPhaseCommit) {
		this.threadedTwoPhaseCommit = threadedTwoPhaseCommit;
		set("threaded_2pc", threadedTwoPhaseCommit);
	}

	public boolean isThreadedTwoPhaseCommit() {
		return this.threadedTwoPhaseCommit;
	}

	private void set(String key, Object value) {
		set("com.atomikos.icatch.", key, value);
	}

	private void set(String keyPrefix, String key, Object value) {
		if (value != null) {
			this.values.put(keyPrefix + key, value.toString());
		}
		else {
			this.values.remove(keyPrefix + key);
		}
	}

	/**
	 * Returns the properties as a {@link Properties} object that can be used with
	 * Atomikos.
	 * @return the properties
	 */
	public Properties asProperties() {
		Properties properties = new Properties();
		properties.putAll(this.values);
		return properties;
	}

}
