package org.jenkinsci.plugins.p4;

import hudson.AbortException;
import hudson.FilePath.FileCallable;
import hudson.model.TaskListener;
import hudson.remoting.VirtualChannel;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.Date;

import org.jenkinsci.plugins.p4.client.ClientHelper;
import org.jenkinsci.plugins.p4.client.ConnectionHelper;
import org.jenkinsci.plugins.p4.credentials.P4StandardCredentials;
import org.jenkinsci.plugins.p4.populate.Populate;
import org.jenkinsci.plugins.p4.review.ReviewProp;
import org.jenkinsci.plugins.p4.workspace.Workspace;

import com.perforce.p4java.impl.generic.core.Label;

public class CheckoutTask implements FileCallable<Boolean>, Serializable {

	private static final long serialVersionUID = 1L;

	private static Logger logger = Logger.getLogger(CheckoutTask.class
			.getName());

	private final P4StandardCredentials credential;
	private final TaskListener listener;
	private final String client;

	private CheckoutStatus status;
	private int head;
	private Object buildChange;
	private int review;
	private Populate populate;

	/**
	 * Constructor
	 * 
	 * @param config
	 *            - Server connection details
	 * @param auth
	 *            - Server login details
	 */
	public CheckoutTask(String credentialID, Workspace config,
			TaskListener listener) {
		this.credential = ConnectionHelper.findCredential(credentialID);
		this.listener = listener;
		this.client = config.getFullName();
	}

	public void setBuildOpts(Workspace workspace) throws AbortException {

		ClientHelper p4 = new ClientHelper(credential, listener, client);

		try {
			// setup the client workspace to use for the build.
			if (!p4.setClient(workspace)) {
				String err = "Undefined workspace: " + workspace.getFullName();
				logger.severe(err);
				listener.error(err);
				throw new AbortException(err);
			}

			// fetch and calculate change to sync to or review to unshelve.
			status = getStatus(workspace);
			head = p4.getClientHead();
			review = getReview(workspace);
			buildChange = getBuildChange(workspace);

			// try to get change-number if automatic label
			if (buildChange instanceof String) {
				String label = (String) buildChange;
				if (p4.isLabel(label)) {
					Label labelSpec = p4.getLabel(label);
					String revSpec = labelSpec.getRevisionSpec();
					if (revSpec != null && !revSpec.isEmpty()
							&& revSpec.startsWith("@")) {
						try {
							int change = Integer.parseInt(revSpec.substring(1));
							buildChange = change;
						} catch (NumberFormatException e) {
							// leave buildChange as is
						}
					}
				}
			}

		} catch (Exception e) {
			String err = "Unable to setup workspace: " + e;
			logger.severe(err);
			listener.error(err);
			throw new AbortException(err);
		} finally {
			p4.disconnect();
		}
	}

	public void setPopulateOpts(Populate populate) {
		this.populate = populate;
	}

	/**
	 * Invoke sync on build node (master or remote node).
	 * 
	 * @return true if updated, false if no change.
	 */
	public Boolean invoke(File workspace, VirtualChannel channel)
			throws IOException {

		ClientHelper p4 = new ClientHelper(credential, listener, client);

		try {
			// test server connection
			if (!p4.isConnected()) {
				p4.log("P4: Server connection error:" + credential.getP4port());
				return false;
			}
			p4.log("Connected to server: " + credential.getP4port());

			// test client connection
			if (p4.getClient() == null) {
				p4.log("P4: Client unknown: " + client);
				return false;
			}
			p4.log("Connected to client: " + client);

			long lStartTime = new Date().getTime();
			
			// Tidy the workspace before sync/build
			p4.tidyWorkspace(populate);
			p4.log("SCM Task: Prepare duration : " + (new Date().getTime() - lStartTime)/1000+" s");
			
			lStartTime = new Date().getTime();
			// Sync workspace to label, head or specified change
			p4.syncFiles(buildChange, populate);

			p4.log("SCM Task: Sync duration : " + (new Date().getTime() - lStartTime)/1000+" s");
			
			// Unshelve review if specified
			if (status == CheckoutStatus.SHELVED) {
				p4.unshelveFiles(review);
			}
		} catch (Exception e) {
			String msg = "Unable to update workspace: " + e;
			logger.warning(msg);
			throw new AbortException(msg);
		} finally {
			p4.disconnect();
		}
		return true;
	}

	/**
	 * Get the build status for the parameter map.
	 * 
	 * @param map
	 * @return
	 */
	private CheckoutStatus getStatus(Workspace workspace) {
		CheckoutStatus status = CheckoutStatus.HEAD;
		String value = workspace.get(ReviewProp.STATUS.toString());
		if (value != null && !value.isEmpty()) {
			status = CheckoutStatus.parse(value);
		}
		return status;
	}

	/**
	 * Get the sync point from the parameter map. Returns the head if no change
	 * found in the map.
	 * 
	 * @param map
	 * @return
	 */
	private Object getBuildChange(Workspace workspace) {
		// Use head as the default
		Object build = this.head;

		// if change is specified then update
		String change = workspace.get(ReviewProp.CHANGE.toString());
		if (change != null && !change.isEmpty()) {
			try {
				build = Integer.parseInt(change);
			} catch (NumberFormatException e) {
			}
		}

		// if label is specified then update
		String label = workspace.get(ReviewProp.LABEL.toString());
		if (label != null && !label.isEmpty()) {
			try {
				// if build is a change-number passed as a label
				build = Integer.parseInt(label);
			} catch (NumberFormatException e) {
				build = label;
			}
		}

		return build;
	}
	
	/**
	 * Get the unshelve point from the parameter map.
	 * 
	 * @param map
	 * @return
	 */
	private int getReview(Workspace workspace) {
		int review = 0;
		String value = workspace.get(ReviewProp.REVIEW.toString());
		if (value != null && !value.isEmpty()) {
			try {
				review = Integer.parseInt(value);
			} catch (NumberFormatException e) {
			}
		}
		return review;
	}

	public List<Integer> getChanges(Object last) {

		List<Integer> changes = new ArrayList<Integer>();

		// Add changes to this build.
		ClientHelper p4 = new ClientHelper(credential, listener, client);
		try {
			changes = p4.listChanges(last, buildChange);
		} catch (Exception e) {
			String err = "Unable to get changes: " + e;
			logger.severe(err);
			listener.getLogger().println(err);
			e.printStackTrace();
		} finally {
			p4.disconnect();
		}

		// Include shelf if a review
		if (status == CheckoutStatus.SHELVED) {
			changes.add(review);
		}

		return changes;
	}

	public CheckoutStatus getStatus() {
		return status;
	}

	public Object getBuildChange() {
		if (status == CheckoutStatus.SHELVED) {
			return review;
		}
		return buildChange;
	}
}