/*
 * Copyright 2012 gitblit.com.
 *
 * 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 com.gitblit.models;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.utils.TimeUtils;

/**
 * The Gitblit Issue model, its component classes, and enums.
 * 
 * @author James Moger
 * 
 */
public class IssueModel implements Serializable, Comparable<IssueModel> {

	private static final long serialVersionUID = 1L;

	public String id;

	public Type type;

	public Status status;

	public Priority priority;

	public Date created;

	public String summary;

	public String description;

	public String reporter;

	public String owner;

	public String milestone;

	public List<Change> changes;

	public IssueModel() {
		// the first applied change set the date appropriately
		created = new Date(0);

		type = Type.Defect;
		status = Status.New;
		priority = Priority.Medium;

		changes = new ArrayList<Change>();
	}

	public String getStatus() {
		String s = status.toString();
		if (!StringUtils.isEmpty(owner))
			s += " (" + owner + ")";
		return s;
	}

	public boolean hasLabel(String label) {
		return getLabels().contains(label);
	}

	public List<String> getLabels() {
		List<String> list = new ArrayList<String>();
		String labels = null;
		for (Change change : changes) {
			if (change.hasField(Field.Labels)) {
				labels = change.getString(Field.Labels);
			}
		}
		if (!StringUtils.isEmpty(labels)) {
			list.addAll(StringUtils.getStringsFromValue(labels, " "));
		}
		return list;
	}

	public Attachment getAttachment(String name) {
		Attachment attachment = null;
		for (Change change : changes) {
			if (change.hasAttachments()) {
				Attachment a = change.getAttachment(name);
				if (a != null) {
					attachment = a;
				}
			}
		}
		return attachment;
	}

	public List<Attachment> getAttachments() {
		List<Attachment> list = new ArrayList<Attachment>();
		for (Change change : changes) {
			if (change.hasAttachments()) {
				list.addAll(change.attachments);
			}
		}
		return list;
	}

	public void applyChange(Change change) {
		if (changes.size() == 0) {
			// first change created the issue
			created = change.created;
		}
		changes.add(change);

		if (change.hasFieldChanges()) {
			for (FieldChange fieldChange : change.fieldChanges) {
				switch (fieldChange.field) {
				case Id:
					id = fieldChange.value.toString();
					break;
				case Type:
					type = IssueModel.Type.fromObject(fieldChange.value);
					break;
				case Status:
					status = IssueModel.Status.fromObject(fieldChange.value);
					break;
				case Priority:
					priority = IssueModel.Priority.fromObject(fieldChange.value);
					break;
				case Summary:
					summary = fieldChange.value.toString();
					break;
				case Description:
					description = fieldChange.value.toString();
					break;
				case Reporter:
					reporter = fieldChange.value.toString();
					break;
				case Owner:
					owner = fieldChange.value.toString();
					break;
				case Milestone:
					milestone = fieldChange.value.toString();
					break;
				}
			}
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("issue ");
		sb.append(id.substring(0, 8));
		sb.append(" (" + summary + ")\n");
		for (Change change : changes) {
			sb.append(change);
			sb.append('\n');
		}
		return sb.toString();
	}

	@Override
	public int compareTo(IssueModel o) {
		return o.created.compareTo(created);
	}

	@Override
	public boolean equals(Object o) {
		if (o instanceof IssueModel)
			return id.equals(((IssueModel) o).id);
		return super.equals(o);
	}

	@Override
	public int hashCode() {
		return id.hashCode();
	}

	public static class Change implements Serializable, Comparable<Change> {

		private static final long serialVersionUID = 1L;

		public final Date created;

		public final String author;

		public String id;

		public char code;

		public Comment comment;

		public Set<FieldChange> fieldChanges;

		public Set<Attachment> attachments;

		public Change(String author) {
			this.created = new Date((System.currentTimeMillis() / 1000) * 1000);
			this.author = author;
			this.id = StringUtils.getSHA1(created.toString() + author);
		}

		public boolean hasComment() {
			return comment != null && !comment.deleted;
		}

		public void comment(String text) {
			comment = new Comment(text);
			comment.id = StringUtils.getSHA1(created.toString() + author + text);
		}

		public boolean hasAttachments() {
			return !ArrayUtils.isEmpty(attachments);
		}

		public void addAttachment(Attachment attachment) {
			if (attachments == null) {
				attachments = new LinkedHashSet<Attachment>();
			}
			attachments.add(attachment);
		}

		public Attachment getAttachment(String name) {
			for (Attachment attachment : attachments) {
				if (attachment.name.equalsIgnoreCase(name)) {
					return attachment;
				}
			}
			return null;
		}

		public boolean hasField(Field field) {
			return !StringUtils.isEmpty(getString(field));
		}

		public boolean hasFieldChanges() {
			return !ArrayUtils.isEmpty(fieldChanges);
		}

		public Object getField(Field field) {
			if (fieldChanges != null) {
				for (FieldChange fieldChange : fieldChanges) {
					if (fieldChange.field == field) {
						return fieldChange.value;
					}
				}
			}
			return null;
		}

		public void setField(Field field, Object value) {
			FieldChange fieldChange = new FieldChange(field, value);
			if (fieldChanges == null) {
				fieldChanges = new LinkedHashSet<FieldChange>();
			}
			fieldChanges.add(fieldChange);
		}

		public String getString(Field field) {
			Object value = getField(field);
			if (value == null) {
				return null;
			}
			return value.toString();
		}

		@Override
		public int compareTo(Change c) {
			return created.compareTo(c.created);
		}

		@Override
		public int hashCode() {
			return id.hashCode();
		}

		@Override
		public boolean equals(Object o) {
			if (o instanceof Change) {
				return id.equals(((Change) o).id);
			}
			return false;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();			
			sb.append(new TimeUtils().timeAgo(created));
			switch (code) {
			case '+':
				sb.append(" created by ");
				break;
			default:
				if (hasComment()) {
					sb.append(" commented on by ");
				} else {
					sb.append(" changed by ");
				}
			}
			sb.append(author).append(" - ");
			if (hasComment()) {
				if (comment.deleted) {
					sb.append("(deleted) ");
				}
				sb.append(comment.text).append(" ");
			}
			if (hasFieldChanges()) {
				switch (code) {
				case '+':
					break;
				default:
					for (FieldChange fieldChange : fieldChanges) {
						sb.append("\n  ");
						sb.append(fieldChange);
					}
					break;
				}
			}
			return sb.toString();
		}
	}

	public static class Comment implements Serializable {

		private static final long serialVersionUID = 1L;

		public String text;

		public String id;

		public boolean deleted;

		Comment(String text) {
			this.text = text;
		}

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

	public static class FieldChange implements Serializable {

		private static final long serialVersionUID = 1L;

		public final Field field;

		public final Object value;

		FieldChange(Field field, Object value) {
			this.field = field;
			this.value = value;
		}

		@Override
		public int hashCode() {
			return field.hashCode();
		}

		@Override
		public boolean equals(Object o) {
			if (o instanceof FieldChange) {
				return field.equals(((FieldChange) o).field);
			}
			return false;
		}

		@Override
		public String toString() {
			return field + ": " + value;
		}
	}

	public static class Attachment implements Serializable {

		private static final long serialVersionUID = 1L;

		public final String name;
		public String id;
		public long size;
		public byte[] content;
		public boolean deleted;

		public Attachment(String name) {
			this.name = name;
		}

		@Override
		public int hashCode() {
			return name.hashCode();
		}

		@Override
		public boolean equals(Object o) {
			if (o instanceof Attachment) {
				return name.equalsIgnoreCase(((Attachment) o).name);
			}
			return false;
		}

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

	public static enum Field {
		Id, Summary, Description, Reporter, Owner, Type, Status, Priority, Milestone, Component, Labels;
	}

	public static enum Type {
		Defect, Enhancement, Task, Review, Other;

		public static Type fromObject(Object o) {
			if (o instanceof Type) {
				// cast and return
				return (Type) o;
			} else if (o instanceof String) {
				// find by name
				for (Type type : values()) {
					String str = o.toString();
					if (type.toString().equalsIgnoreCase(str)) {
						return type;
					}
				}
			} else if (o instanceof Number) {
				// by ordinal
				int id = ((Number) o).intValue();
				if (id >= 0 && id < values().length) {
					return values()[id];
				}
			}
			return null;
		}
	}

	public static enum Priority {
		Low, Medium, High, Critical;

		public static Priority fromObject(Object o) {
			if (o instanceof Priority) {
				// cast and return
				return (Priority) o;
			} else if (o instanceof String) {
				// find by name
				for (Priority priority : values()) {
					String str = o.toString();
					if (priority.toString().equalsIgnoreCase(str)) {
						return priority;
					}
				}
			} else if (o instanceof Number) {
				// by ordinal
				int id = ((Number) o).intValue();
				if (id >= 0 && id < values().length) {
					return values()[id];
				}
			}
			return null;
		}
	}

	public static enum Status {
		New, Accepted, Started, Review, Queued, Testing, Done, Fixed, WontFix, Duplicate, Invalid;

		public static Status fromObject(Object o) {
			if (o instanceof Status) {
				// cast and return
				return (Status) o;
			} else if (o instanceof String) {
				// find by name
				for (Status status : values()) {
					String str = o.toString();
					if (status.toString().equalsIgnoreCase(str)) {
						return status;
					}
				}
			} else if (o instanceof Number) {
				// by ordinal
				int id = ((Number) o).intValue();
				if (id >= 0 && id < values().length) {
					return values()[id];
				}
			}
			return null;
		}

		public boolean atLeast(Status status) {
			return ordinal() >= status.ordinal();
		}

		public boolean exceeds(Status status) {
			return ordinal() > status.ordinal();
		}

		public boolean isClosed() {
			return ordinal() >= Done.ordinal();
		}

		public Status next() {
			switch (this) {
			case New:
				return Started;
			case Accepted:
				return Started;
			case Started:
				return Testing;
			case Review:
				return Testing;
			case Queued:
				return Testing;
			case Testing:
				return Done;
			}
			return Accepted;
		}
	}
}
