/*******************************************************************************
 * Copyright (c) 2010 Red Hat Inc. and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Red Hat Inc. - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.internal.bugzilla.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.apache.xmlrpc.XmlRpcException;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.internal.bugzilla.core.service.BugzillaXmlRpcClient;
import org.eclipse.mylyn.tasks.core.data.TaskAttribute;

/**
 * @author Charley Wang
 */
public class CustomTransitionManager implements Serializable {
	private static final long serialVersionUID = 3340305752692674487L;

	private static final String DEFAULT_DUPLICATE_STATUS = "RESOLVED"; //$NON-NLS-1$

	/** Default start status -- this uses the Mylyn default from before bug #317729 */
	public static final String DEFAULT_START_STATUS = "NEW"; //$NON-NLS-1$

	/*
	 * Create the default Bugzilla operations
	 */

	//Indexed by the current status
	private final HashMap<String, List<AbstractBugzillaOperation>> operationMapByCurrentStatus;

	//Indexed by the status to be transitioned into
	private final HashMap<String, List<AbstractBugzillaOperation>> operationMapByEndStatus;

	//Statuses that are not marked as is_open by Bugzilla
	private final ArrayList<String> closedStatuses;

	//Whether or not customized names were used in Bugzilla
	private boolean customNames = false;

	private boolean valid = false;

	private String filePath;

	private String duplicateStatus;

	private String startStatus;

	public CustomTransitionManager() {
		operationMapByCurrentStatus = new HashMap<String, List<AbstractBugzillaOperation>>();
		operationMapByEndStatus = new HashMap<String, List<AbstractBugzillaOperation>>();
		closedStatuses = new ArrayList<String>();
		this.valid = false;
		this.filePath = ""; //$NON-NLS-1$
		duplicateStatus = DEFAULT_DUPLICATE_STATUS;
		startStatus = DEFAULT_START_STATUS;
	}

	/**
	 * Searches for a valid transition description file. Returns true if a file exists and was sucessfully parsed, false
	 * otherwise
	 * 
	 * @param filePath
	 * @return true if anything was changed, false otherwise.
	 * @throws CoreException
	 */
	public boolean parse(String filePath) throws CoreException {
		if (filePath == null || filePath.length() < 1) {
			setValid(false);
			return false;
		} else if (filePath.equals(this.filePath)) {
			//Do nothing, already parsed this file
			return false;
		}
		this.filePath = filePath;
		setValid(true);

		operationMapByCurrentStatus.clear();
		operationMapByEndStatus.clear();
		closedStatuses.clear();

		File file = new File(filePath);
		if (!file.exists() || !file.canRead()) {
			setValid(false);
			return isValid();
		}

		BufferedReader br = null;
		try {
			String s;
			boolean checkOptions = true;
			br = new BufferedReader(new FileReader(file));

			while ((s = br.readLine()) != null && isValid()) {
				if (s.equals("<transitions>")) { //$NON-NLS-1$
					checkOptions = false;
					defaultNames();
					continue;
				}

				if (checkOptions) {
					parseOptions(s);
				} else {
					parseTransitions(s);
				}
			}

		} catch (IOException e) {
			setValid(false);
			throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 1,
					"Error parsing transition description file.\n\n" + e.getMessage(), e)); //$NON-NLS-1$
		} finally {
			if (br != null) {
				try {
					br.close();
				} catch (IOException e) {
				}
			}
		}

		return valid;
	}

	private void parseOptions(String s) throws IOException {
		String[] pieces = s.split("="); //$NON-NLS-1$
		if (pieces.length != 2) {
			throw new IOException(Messages.CustomTransitionManager_InvalidBugzillaOption + s);
		}

		String name = pieces[0];
		String value = pieces[1];

		if (name.equals("CustomStatusNames")) { //$NON-NLS-1$
			if (value.equals("true")) { //$NON-NLS-1$
				customNames = true;
			} else {
				customNames = false;
			}
		} else if (name.equals("DuplicateStatus")) { //$NON-NLS-1$
			duplicateStatus = value;
		} else if (name.equals("ClosedCustomStatus")) { //$NON-NLS-1$
			closedStatuses.add(value);
		} else if (name.equals("StartStatus")) {//$NON-NLS-1$
			startStatus = value;
		}
	}

	private void parseTransitions(String s) throws IOException {
		String[] pieces = s.split(":"); //$NON-NLS-1$
		if (pieces.length < 4) {
			throw new IOException(Messages.CustomTransitionManager_InvalidBugzillaTransition + s);
		}
		String status = pieces[1];
		String[] endStatuses = pieces[3].split(","); //$NON-NLS-1$
		parse(status, endStatuses);
	}

	private void defaultNames() {

		/*
		 * Add the default operations if we are not using custom names
		 */

		//Step 1: Make custom operations for the default operations
		//This is necessary so we can add verifiers to these operations later
		if (operationMapByEndStatus.size() == 0) {

			ArrayList<AbstractBugzillaOperation> list = new ArrayList<AbstractBugzillaOperation>();

			//NONE is currently not used from the map, included for completeness
			list.add(BugzillaOperation.none);
			operationMapByEndStatus.put("NONE", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.accept);
			operationMapByEndStatus.put("ASSIGNED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.resolve);
			if (duplicateStatus.equals("RESOLVED")) { //$NON-NLS-1$
				list.add(BugzillaOperation.duplicate);
			}
			operationMapByEndStatus.put("RESOLVED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.reopen);
			operationMapByEndStatus.put("REOPENED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.reopen);
			operationMapByEndStatus.put("UNCONFIRMED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.verify);
			if (duplicateStatus.equals("VERIFIED")) { //$NON-NLS-1$
				list.add(BugzillaOperation.duplicate);
			}
			operationMapByEndStatus.put("VERIFIED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.close);
			list.add(BugzillaOperation.close_with_resolution);
			if (duplicateStatus.equals("CLOSED")) { //$NON-NLS-1$
				list.add(BugzillaOperation.duplicate);
			}
			operationMapByEndStatus.put("CLOSED", list); //$NON-NLS-1$

			list = new ArrayList<AbstractBugzillaOperation>();
			list.add(BugzillaOperation.markNew);
			operationMapByEndStatus.put("NEW", list); //$NON-NLS-1$
		}

	}

	public List<AbstractBugzillaOperation> getOperation(String id) {
		return operationMapByEndStatus.get(id);
	}

	public List<AbstractBugzillaOperation> getValidTransitions(String key) {
		return operationMapByCurrentStatus.get(key);
	}

	/**
	 * Sets whether or not this class is valid. If set to false, the filePath will be set to "" so that subsequent calls
	 * to parse(filePath) will work.
	 * 
	 * @param val
	 */
	public void setValid(boolean val) {
		if (val == false) {
			this.filePath = ""; //$NON-NLS-1$
		}
		this.valid = val;
	}

	public boolean isValid() {
		return this.valid;
	}

	/**
	 * Returns the duplicate status. Standard Bugzilla installations will have a duplicate status of RESOLVED, VERIFIED
	 * or CLOSED. <br>
	 * By default, the duplicate status will be RESOLVED.
	 * 
	 * @return The status to send if a bug is set to Duplicate
	 */
	public String getDuplicateStatus() {
		if (duplicateStatus == null || duplicateStatus.length() == 0) {
			duplicateStatus = DEFAULT_DUPLICATE_STATUS;
		}
		return duplicateStatus;
	}

	/**
	 * Returns the start status. Standard Bugzilla installations have 2 available start statuses, UNCONFIRMED and NEW.
	 * Each user has a permissions setting that determines whether their new bugs start as UNCONFIRMED or NEW. This
	 * permissions setting currently cannot be accessed, so this function does not try to guess and returns either the
	 * default status (NEW) or whichever status was set by a transition file. <br>
	 * Fix for bug #318128, set startStatus to NEW if startStatus is somehow null at this point.
	 * 
	 * @return The valid start status. Default value is NEW.
	 */
	public String getStartStatus() {
		if (startStatus == null || startStatus.length() == 0) {
			startStatus = DEFAULT_START_STATUS;
		}
		return startStatus;
	}

	private void addTransition(String start, String endStatus) {
		if (!customNames
				&& (start.equals("REOPENED") && (endStatus.equals("UNCONFIRMED") || endStatus.equals("REOPENED")))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			//REOPENED should not retransition to REOPENED
			return;
		}
		List<AbstractBugzillaOperation> list = operationMapByCurrentStatus.get(start);
		if (list == null) {
			list = new ArrayList<AbstractBugzillaOperation>();
		}
		list.addAll(operationMapByEndStatus.get(endStatus));
		operationMapByCurrentStatus.put(start, list);
	}

	private void parse(String start, Object[] transitions) {
		addNewStatus(start);

		//Find valid transitions
		for (Object o : transitions) {
			if ((o instanceof HashMap<?, ?>)) {
				//Used for XMLRPC
				HashMap<?, ?> tran = (HashMap<?, ?>) o;
				String endStatus = (String) tran.get("name"); //$NON-NLS-1$
				addNewStatus(endStatus);
				if (!endStatus.equals(start)) {
					addTransition(start, endStatus);
				}
			} else if (o instanceof String) {
				//Used for files
				String endStatus = (String) o;
				addNewStatus(endStatus);
				if (!endStatus.equals(start)) {
					addTransition(start, endStatus);
				}
			}
		}
	}

	public void parse(IProgressMonitor monitor, BugzillaXmlRpcClient xmlClient) throws CoreException {
		this.filePath = ""; //$NON-NLS-1$
		operationMapByCurrentStatus.clear();
		operationMapByEndStatus.clear();
		closedStatuses.clear();
		defaultNames();
		setValid(false);
		//Assume custom names, we have no way to check
		customNames = true;

		try {
			if (xmlClient.getUserID() == -1) {
				xmlClient.login(monitor);
			}
			String[] fields = new String[1];
			fields[0] = "bug_status"; //$NON-NLS-1$
			for (Object raw : xmlClient.getFieldsWithNames(monitor, fields)) {
				if (raw instanceof HashMap<?, ?>) {
					Object[] values = (Object[]) ((HashMap<?, ?>) raw).get("values"); //$NON-NLS-1$
					if (values == null) {
						continue;
					}
					for (Object status : values) {
						if (status instanceof HashMap<?, ?>) {
							//Get name
							HashMap<?, ?> map = (HashMap<?, ?>) status;
							String start = (String) map.get("name"); //$NON-NLS-1$
							//Get is_open
							Object is_open = map.get("is_open"); //$NON-NLS-1$
							if (is_open.toString().equals("false")) { //$NON-NLS-1$
								closedStatuses.add(start);
							}
							parse(start, (Object[]) map.get("can_change_to")); //$NON-NLS-1$

						}
					}
				} else {
					throw new XmlRpcException(Messages.CustomTransitionManager_UnexpectedResponse);
				}
			}
			//Should check if there are conditions we can use to terminate early
			if (operationMapByCurrentStatus.size() == 0) {
				throw new XmlRpcException(Messages.CustomTransitionManager_UnexpectedResponse);
			}

			setValid(true);
		} catch (XmlRpcException e) {
			setValid(false);
			String message = e.linkedException == null ? e.getMessage() : e.getMessage() + ">" //$NON-NLS-1$
					+ e.linkedException.getMessage();
			throw new CoreException(new Status(IStatus.ERROR, BugzillaCorePlugin.ID_PLUGIN, 1,
					"Error parsing xmlrpc response.\n\n" + message, e)); //$NON-NLS-1$
		}
	}

	/**
	 * Creates a new status with a single operation with the same name as the status itself. Does nothing if a status of
	 * that name already exists.
	 * 
	 * @param status
	 */
	private void addNewStatus(String status) {
		List<AbstractBugzillaOperation> list = operationMapByEndStatus.get(status);
		if (list == null) {
			list = new ArrayList<AbstractBugzillaOperation>();
		} else {
			return;
		}

		if (!closedStatuses.contains(status)) {
			list.add(new BugzillaOperation(AbstractBugzillaOperation.DEFAULT_LABEL_PREFIX + status));
		} else {
			list.add(new BugzillaOperation(AbstractBugzillaOperation.DEFAULT_LABEL_PREFIX + status, "resolution", //$NON-NLS-1$
					TaskAttribute.TYPE_SINGLE_SELECT, status));
		}

		operationMapByEndStatus.put(status, list);
	}

	public ArrayList<String> getClosedStatuses() {
		return closedStatuses;
	}

}
