/*******************************************************************************
 * Copyright (c) 2011, 2012 Tasktop Technologies 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:
 *     Tasktop Technologies - initial API and implementation
 *******************************************************************************/

package org.eclipse.mylyn.tasks.core.data;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.eclipse.core.runtime.Assert;

/**
 * Base class for task schemas. Clients should subclass to define a specific schema.
 *
 * @author Steffen Pingel
 * @author David Green
 * @author Miles Parker
 * @since 3.5
 */
public abstract class AbstractTaskSchema {

	public static class Field {

		private EnumSet<Flag> flags;

		private final String key;

		private final String label;

		private final String type;

		private final String indexKey;

		protected Field(String key, String label, String type) {
			this(key, label, type, null, (Flag[]) null);
		}

		protected Field(String key, String label, String type, Flag... flags) {
			this(key, label, type, null, flags);
		}

		/**
		 * @param key
		 *            the task attribute key, which may be a common task attribute key defined in defined in
		 *            {@link TaskAttribute}
		 * @param label
		 *            the user-visible label that is used by the user to identify this field
		 * @param type
		 *            the type of the field, should be one of the constants defined in TaskAttribute (
		 *            <code>TaskAttribute.TYPE_*</code>)
		 * @param indexKey
		 *            the index key, or null if this should not be indexed
		 * @param flags
		 *            the flags, or null
		 * @since 3.7
		 */
		public Field(String key, String label, String type, String indexKey, Flag... flags) {
			Assert.isNotNull(key);
			Assert.isNotNull(label);
			Assert.isNotNull(type);
			this.key = key;
			this.label = label;
			this.type = type;
			this.indexKey = indexKey;
			if (flags == null || flags.length == 0) {
				this.flags = EnumSet.noneOf(Flag.class);
			} else {
				this.flags = EnumSet.copyOf(Arrays.asList(flags));
			}
		}

		public TaskAttribute createAttribute(TaskAttribute parent) {
			TaskAttribute attribute = parent.createMappedAttribute(getKey());
			// meta data
			TaskAttributeMetaData metaData = attribute.getMetaData();
			metaData.setLabel(getLabel());
			metaData.setType(getType());
			metaData.setReadOnly(isReadOnly());
			metaData.setKind(getKind());
			metaData.setRequired(isRequired());
			// options
			Map<String, String> options = getDefaultOptions();
			if (options != null) {
				for (Entry<String, String> option : options.entrySet()) {
					attribute.putOption(option.getKey(), option.getValue());
				}
			}
			return attribute;
		}

		public Map<String, String> getDefaultOptions() {
			return Collections.emptyMap();
		}

		public String getKey() {
			return key;
		}

		/**
		 * the key to use when indexing this field
		 *
		 * @return the index key, or null if this should not be indexed
		 * @since 3.7
		 */
		public String getIndexKey() {
			return indexKey;
		}

		public String getKind() {
			if (flags.contains(Flag.ATTRIBUTE)) {
				return TaskAttribute.KIND_DEFAULT;
			} else if (flags.contains(Flag.PEOPLE)) {
				return TaskAttribute.KIND_PEOPLE;
			} else if (flags.contains(Flag.OPERATION)) {
				return TaskAttribute.KIND_OPERATION;
			} else if (flags.contains(Flag.DESCRIPTION)) {
				return TaskAttribute.KIND_DESCRIPTION;
			}
			return null;
		}

		public String getLabel() {
			return label;
		}

		public String getType() {
			return type;
		}

		public boolean isReadOnly() {
			return flags.contains(Flag.READ_ONLY);
		}

		@Override
		public String toString() {
			return getLabel();
		}

		/**
		 * @since 3.7
		 */
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((indexKey == null) ? 0 : indexKey.hashCode());
			result = prime * result + ((key == null) ? 0 : key.hashCode());
			return result;
		}

		/**
		 * @since 3.7
		 */
		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			Field other = (Field) obj;
			if (indexKey == null) {
				if (other.indexKey != null) {
					return false;
				}
			} else if (!indexKey.equals(other.indexKey)) {
				return false;
			}
			if (key == null) {
				if (other.key != null) {
					return false;
				}
			} else if (!key.equals(other.key)) {
				return false;
			}
			return true;
		}

		/**
		 * @since 3.11
		 */
		public boolean isRequired() {
			return flags.contains(Flag.REQUIRED);
		}

	}

	public enum Flag {
		ATTRIBUTE, OPERATION, PEOPLE, READ_ONLY,
		/**
		 * A flag used to indicate that the field is related to a description.
		 *
		 * @since 3.11
		 * @see TaskAttribute#KIND_DESCRIPTION
		 */
		DESCRIPTION,
		/**
		 * A flag used to indicate that the field is required.
		 *
		 * @since 3.11
		 * @see TaskAttribute#META_REQUIRED
		 */
		REQUIRED

	};

	protected class FieldFactory {

		private EnumSet<Flag> flags;

		private String key;

		private String label;

		private String type;

		public FieldFactory(Field source) {
			this.flags = EnumSet.copyOf(source.flags);
			this.key = source.key;
			this.label = source.label;
			this.type = source.type;
		}

		public FieldFactory addFlags(Flag... flags) {
			this.flags.addAll(Arrays.asList(flags));
			return this;
		}

		public Field create() {
			return createField(key, label, type, (!flags.isEmpty()) ? flags.toArray(new Flag[0]) : null);
		}

		public FieldFactory flags(Flag... flags) {
			this.flags = EnumSet.copyOf(Arrays.asList(flags));
			return this;
		}

		public FieldFactory key(String key) {
			this.key = key;
			return this;
		}

		public FieldFactory label(String label) {
			this.label = label;
			return this;
		}

		public FieldFactory removeFlags(Flag... flags) {
			this.flags.removeAll(Arrays.asList(flags));
			return this;
		}

		public FieldFactory type(String type) {
			this.type = type;
			return this;
		}

	}

	private final Map<String, Field> fieldByKey = new LinkedHashMap<String, Field>();

	/**
	 * Returns the specified field for the given key.
	 */
	public Field getFieldByKey(String taskKey) {
		return fieldByKey.get(taskKey);
	}

	/**
	 * Creates no-value attributes with default options for the supplied task for each schema field.
	 */
	public void initialize(TaskData taskData) {
		for (Field field : fieldByKey.values()) {
			field.createAttribute(taskData.getRoot());
		}
	}

	/**
	 * Provides an iterator for all fields within the schema. Subsequent modifications to the returned collection are
	 * not reflected to schema.
	 *
	 * @since 3.9
	 * @return all fields within the schema
	 */
	public Collection<Field> getFields() {
		return new ArrayList<Field>(fieldByKey.values());
	}

	protected Field createField(String key, String label, String type) {
		return createField(key, label, type, null, (Flag[]) null);
	}

	protected Field createField(String key, String label, String type, Flag... flags) {
		return createField(key, label, type, null, flags);
	}

	/**
	 * @since 3.7
	 * @see Field#Field(String, String, String, String, Flag...)
	 */
	protected Field createField(String key, String label, String type, String indexKey, Flag... flags) {
		Field field = new Field(key, label, type, indexKey, flags);
		fieldByKey.put(key, field);
		return field;
	}

	protected FieldFactory inheritFrom(Field source) {
		return new FieldFactory(source);
	}

}
