/*
 * Copyright 2006-2023 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
 *
 *      https://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.batch.core;

import org.springframework.util.StringUtils;

import java.io.PrintWriter;
import java.io.Serializable;
import java.io.StringWriter;

/**
 * Value object used to carry information about the status of a job or step execution.
 * <p>
 * {@code ExitStatus} is immutable and, therefore, thread-safe.
 *
 * @author Dave Syer
 * @author Mahmoud Ben Hassine
 *
 */
public class ExitStatus implements Serializable, Comparable<ExitStatus> {

	/**
	 * Convenient constant value representing unknown state - assumed to not be
	 * continuable.
	 */
	public static final ExitStatus UNKNOWN = new ExitStatus("UNKNOWN");

	/**
	 * Convenient constant value representing continuable state where processing is still
	 * taking place, so no further action is required. Used for asynchronous execution
	 * scenarios where the processing is happening in another thread or process and the
	 * caller is not required to wait for the result.
	 */
	public static final ExitStatus EXECUTING = new ExitStatus("EXECUTING");

	/**
	 * Convenient constant value representing finished processing.
	 */
	public static final ExitStatus COMPLETED = new ExitStatus("COMPLETED");

	/**
	 * Convenient constant value representing a job that did no processing (for example,
	 * because it was already complete).
	 */
	public static final ExitStatus NOOP = new ExitStatus("NOOP");

	/**
	 * Convenient constant value representing finished processing with an error.
	 */
	public static final ExitStatus FAILED = new ExitStatus("FAILED");

	/**
	 * Convenient constant value representing finished processing with interrupted status.
	 */
	public static final ExitStatus STOPPED = new ExitStatus("STOPPED");

	private final String exitCode;

	private final String exitDescription;

	/**
	 * Constructor that accepts the exit code and sets the exit description to an empty
	 * {@link String}.
	 * @param exitCode The exit code to be used for the {@link ExitStatus}.
	 */
	public ExitStatus(String exitCode) {
		this(exitCode, "");
	}

	/**
	 * Constructor that establishes the exit code and the exit description for the
	 * {@link ExitStatus}.
	 * @param exitCode The exit code to be used for the {@link ExitStatus}.
	 * @param exitDescription The exit description to be used for the {@link ExitStatus}.
	 */
	public ExitStatus(String exitCode, String exitDescription) {
		super();
		this.exitCode = exitCode;
		this.exitDescription = exitDescription == null ? "" : exitDescription;
	}

	/**
	 * Getter for the exit code (defaults to blank).
	 * @return the exit code.
	 */
	public String getExitCode() {
		return exitCode;
	}

	/**
	 * Getter for the exit description (defaults to blank)
	 * @return {@link String} containing the exit description.
	 */
	public String getExitDescription() {
		return exitDescription;
	}

	/**
	 * Create a new {@link ExitStatus} with a logical combination of the exit code and a
	 * concatenation of the descriptions. If either value has a higher severity, its exit
	 * code is used in the result. In the case of equal severity, the exit code is
	 * replaced if the new value is alphabetically greater.<br>
	 * <br>
	 *
	 * Severity is defined by the exit code:
	 * <ul>
	 * <li>Codes beginning with EXECUTING have severity 1</li>
	 * <li>Codes beginning with COMPLETED have severity 2</li>
	 * <li>Codes beginning with NOOP have severity 3</li>
	 * <li>Codes beginning with STOPPED have severity 4</li>
	 * <li>Codes beginning with FAILED have severity 5</li>
	 * <li>Codes beginning with UNKNOWN have severity 6</li>
	 * </ul>
	 * Others have severity 7, so custom exit codes always win.<br>
	 *
	 * If the input is {@code null} just return this.
	 * @param status An {@link ExitStatus} object to combine with this one.
	 * @return a new {@link ExitStatus} combining the current value and the argument
	 * provided.
	 */
	public ExitStatus and(ExitStatus status) {
		if (status == null) {
			return this;
		}
		ExitStatus result = addExitDescription(status.exitDescription);
		if (compareTo(status) < 0) {
			result = result.replaceExitCode(status.exitCode);
		}
		return result;
	}

	/**
	 * @param status An {@link ExitStatus} to compare
	 * @return greater than zero, 0, or less than zero, according to the severity and exit
	 * code.
	 * @see java.lang.Comparable
	 */
	@Override
	public int compareTo(ExitStatus status) {
		if (severity(status) > severity(this)) {
			return -1;
		}
		if (severity(status) < severity(this)) {
			return 1;
		}
		return this.getExitCode().compareTo(status.getExitCode());
	}

	/**
	 * Determines severity (an int between 1 and 7, inclusive) based on an
	 * {@code ExitStatus} object.
	 * @param status The {@code ExitStatus} object from which to determine the severity.
	 * @return the severity number.
	 */
	private int severity(ExitStatus status) {
		if (status.exitCode.startsWith(EXECUTING.exitCode)) {
			return 1;
		}
		if (status.exitCode.startsWith(COMPLETED.exitCode)) {
			return 2;
		}
		if (status.exitCode.startsWith(NOOP.exitCode)) {
			return 3;
		}
		if (status.exitCode.startsWith(STOPPED.exitCode)) {
			return 4;
		}
		if (status.exitCode.startsWith(FAILED.exitCode)) {
			return 5;
		}
		if (status.exitCode.startsWith(UNKNOWN.exitCode)) {
			return 6;
		}
		return 7;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return String.format("exitCode=%s;exitDescription=%s", exitCode, exitDescription);
	}

	/**
	 * Compare the fields, one by one.
	 *
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (obj == null) {
			return false;
		}
		return toString().equals(obj.toString());
	}

	/**
	 * Compatible with the equals implementation.
	 *
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		return toString().hashCode();
	}

	/**
	 * Add an exit code to an existing {@link ExitStatus}. If there is already a code
	 * present, it will be replaced.
	 * @param code The code to add.
	 * @return a new {@link ExitStatus} with the same properties but a new exit code.
	 */
	public ExitStatus replaceExitCode(String code) {
		return new ExitStatus(code, exitDescription);
	}

	/**
	 * Check if this status represents a running process.
	 * @return {@code true} if the exit code is {@code EXECUTING} or {@code UNKNOWN}.
	 */
	public boolean isRunning() {
		return "EXECUTING".equals(this.exitCode) || "UNKNOWN".equals(this.exitCode);
	}

	/**
	 * Add an exit description to an existing {@link ExitStatus}. If there is already a
	 * description present, the two are concatenated with a semicolon.
	 * @param description The description to add.
	 * @return a new {@link ExitStatus} with the same properties but a new exit
	 * description.
	 */
	public ExitStatus addExitDescription(String description) {
		if (StringUtils.hasText(exitDescription)) {
			if (StringUtils.hasText(description) && !exitDescription.equals(description)) {
				StringBuilder buffer = new StringBuilder(description.length() + 2 + exitDescription.length());
				buffer.append(exitDescription);
				buffer.append("; ");
				buffer.append(description);
				return new ExitStatus(exitCode, buffer.toString());
			}
			return this;
		}
		else {
			return new ExitStatus(exitCode, description);
		}
	}

	/**
	 * Extract the stack trace from the throwable provided and append it to the existing
	 * description.
	 * @param throwable A {@link Throwable} instance containing the stack trace.
	 * @return a new ExitStatus with the stack trace appended.
	 */
	public ExitStatus addExitDescription(Throwable throwable) {
		StringWriter writer = new StringWriter();
		throwable.printStackTrace(new PrintWriter(writer));
		String message = writer.toString();
		return addExitDescription(message);
	}

	/**
	 * @param status The {@code ExitStatus} object containing the exit code to be
	 * evaluated.
	 * @return {@code true} if the value matches a known exit code.
	 */
	public static boolean isNonDefaultExitStatus(ExitStatus status) {
		return status == null || status.getExitCode() == null
				|| status.getExitCode().equals(ExitStatus.COMPLETED.getExitCode())
				|| status.getExitCode().equals(ExitStatus.EXECUTING.getExitCode())
				|| status.getExitCode().equals(ExitStatus.FAILED.getExitCode())
				|| status.getExitCode().equals(ExitStatus.NOOP.getExitCode())
				|| status.getExitCode().equals(ExitStatus.STOPPED.getExitCode())
				|| status.getExitCode().equals(ExitStatus.UNKNOWN.getExitCode());
	}

}
