/*****************************************************************************
 * Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License Version
 * 1.0 (the "License"). You may not use this file except in compliance with
 * the License. A copy of the License is available at http://www.sun.com/
 *
 * The Original Code is the CVS Client Library.
 * The Initial Developer of the Original Code is Robert Greig.
 * Portions created by Robert Greig are Copyright (C) 2000.
 * All Rights Reserved.
 *
 * Contributor(s): Robert Greig.
 *****************************************************************************/
package org.netbeans.lib.cvsclient.admin;

import java.text.*;
import java.util.*;

import org.netbeans.lib.cvsclient.util.*;

/**
 * The class abstracts the CVS concept of an <i>entry line</i>. The entry
 * line is textually of the form:<p>
 * / name / version / conflict / options / tag_or_date
 * <p>These are explained in section 5.1 of the CVS protocol 1.10 document.
 *
 * @author Robert Greig
 */
public final class Entry {

	// Constants ==============================================================

	private static final ILogger LOG = LoggerManager.getLogger("javacvs.entry");

	private static final String DUMMY_TIMESTAMP = "dummy timestamp";
	private static final String DUMMY_TIMESTAMP_NEW_ENTRY = "dummy timestamp from new-entry";
	private static final String MERGE_TIMESTAMP = "Result of merge";

	public static final String STICKY_PREFIX_TAG_OR_REVISION = "N";
	public static final String STICKY_PREFIX_BRANCH_TAG = "T";
	public static final String STICKY_PREFIX_DATE = "D";

	private static final String BINARY_FILE = "-kb";

	private static final String HAD_CONFLICTS = "+";
	private static final char TIMESTAMP_MATCHES_FILE = '=';

	private static final String REVISION_ADDED = "0";
	private static final String REVISION_REMOVED_PREFIX = "-";
	private static final SimpleDateFormat STICKY_DATE_FORMAT = new SimpleDateFormat("yyyy.MM.dd.hh.mm.ss");

	// Static =================================================================

	private static SimpleDateFormat lastModifiedDateFormatter;

	public static synchronized String formatLastModifiedDate(Date date) {
		return getLastModifiedDateFormatter().format(date);
	}

	public static Entry createDirectoryEntry(String directoryName) {
		final Entry entry = new Entry();
		entry.setFileName(directoryName);
		entry.setDirectory(true);
		return entry;
	}

	public static Entry createFileEntry(String fileName) {
		final Entry entry = new Entry();
		entry.setFileName(fileName);
		entry.setDirectory(false);
		return entry;
	}

	public static Entry createEntryForLine(String entryLine) {
		final Entry entry = new Entry();
		entry.parseLine(entryLine);
		return entry;
	}

	// Fields =================================================================

	private boolean directory;
	private String fileName;
	private Date lastModified;
	private String revision;
	private boolean conflict;
	private boolean timeStampMatchesFile;
	private String conflictString;
	private String conflictStringWithoutConflictMarker;
	private String options;

	private String stickyRevision;
	private String stickyTag;
	private String stickyDateString;
	private Date stickyDate;

	// Setup ==================================================================

	private Entry() {
	}

	// Implemented ============================================================

	@Override
	public String toString() {
		final StringBuilder buffer = new StringBuilder();
		if (directory) {
			buffer.append("D/");
		}
		else {
			buffer.append('/');
		}
		// if name is null, then this is a totally empty entry, so append
		// nothing further
		if (fileName != null) {
			buffer.append(fileName);
			buffer.append('/');
			if (revision != null) {
				buffer.append(revision);
			}
			buffer.append('/');
			if (conflictString != null) {
				buffer.append(conflictString);
			}
			buffer.append('/');
			if (options != null) {
				buffer.append(options);
			}
			buffer.append('/');
			if (stickyTag != null) {
				buffer.append(STICKY_PREFIX_BRANCH_TAG);
				buffer.append(stickyTag);
			}
			else if (stickyRevision != null) {
				buffer.append(STICKY_PREFIX_BRANCH_TAG);
				buffer.append(stickyRevision);
			}
			else if (stickyDateString != null) {
				buffer.append(STICKY_PREFIX_DATE);
				buffer.append(stickyDateString);
			}
		}
		return buffer.toString();
	}

	@Override
	public boolean equals(Object obj) {
		if (obj == null || obj.getClass() != getClass()) {
			return false;
		}

		final String entryFileName = ((Entry)obj).fileName;
		return (fileName == entryFileName)
				|| (fileName != null && fileName.equals(entryFileName));
	}

	@Override
	public int hashCode() {
		return (fileName != null)
				? fileName.hashCode()
				: 0;
	}

	// Accessing ==============================================================

	public String getFileName() {
		return fileName;
	}

	public String getRevision() {
		return revision;
	}

	public void setRevision(String revision) {
		this.revision = revision;
	}

	public Date getLastModified() {
		return lastModified;
	}

	public boolean isResultOfMerge() {
		return conflictString != null && conflictString.startsWith(MERGE_TIMESTAMP);
	}

	public void setConflict(String conflictString) {
		this.conflictString = conflictString;
	}

	public String getOptions() {
		return options;
	}

	public String getStickyTag() {
		return stickyTag;
	}

	public void setStickyTag(String stickyTag) {
		this.stickyTag = stickyTag;
		this.stickyRevision = null;
		this.stickyDateString = null;
		this.stickyDate = null;
	}

	public String getStickyRevision() {
		return stickyRevision;
	}

	public void setStickyRevision(String stickyRevision) {
		this.stickyTag = null;
		this.stickyRevision = stickyRevision;
		this.stickyDateString = null;
		this.stickyDate = null;
	}

	public String getStickyDateString() {
		return stickyDateString;
	}

	public void setStickyDateString(String stickyDateString) {
		this.stickyTag = null;
		this.stickyRevision = null;
		this.stickyDateString = stickyDateString;
		this.stickyDate = null;
	}

	public Date getStickyDate() {
		// lazy generation
		if (stickyDate != null) {
			return stickyDate;
		}
		if (stickyDateString == null) {
			return null;
		}

		try {
			return STICKY_DATE_FORMAT.parse(stickyDateString);
		}
		catch (ParseException ex) {
			// ignore silently
			return null;
		}
	}

	public void setStickyDate(Date stickyDate) {
		if (stickyDate == null) {
			this.stickyTag = null;
			this.stickyRevision = null;
			this.stickyDateString = null;
			this.stickyDate = null;
			return;
		}

		this.stickyTag = null;
		this.stickyRevision = null;
		this.stickyDateString = STICKY_DATE_FORMAT.format(stickyDate);
		this.stickyDate = stickyDate;
	}

	public String getStickyOption() {
		if (stickyTag != null) {
			return stickyTag;
		}
		if (stickyRevision != null) {
			return stickyRevision;
		}
		return stickyDateString;
	}

	public void setStickyInformation(Entry entry) {
		stickyTag = entry.stickyTag;
		stickyRevision = entry.stickyRevision;
		stickyDateString = entry.stickyDateString;
		stickyDate = entry.stickyDate;
	}

	public void setStickyInformation(String stickyInformation) {
		if (stickyInformation == null) {
			resetStickyInformation();
			return;
		}

		if (stickyInformation.startsWith(STICKY_PREFIX_BRANCH_TAG)) {
			final String tagOrRevision = stickyInformation.substring(STICKY_PREFIX_BRANCH_TAG.length());
			if (tagOrRevision.length() == 0) {
				resetStickyInformation();
				return;
			}

			final char firstChar = tagOrRevision.charAt(0);
			if (firstChar >= '0' && firstChar <= '9') {
				setStickyRevision(tagOrRevision);
			}
			else {
				setStickyTag(tagOrRevision);
			}
			return;
		}

		if (stickyInformation.startsWith(STICKY_PREFIX_DATE)) {
			setStickyDateString(stickyInformation.substring(STICKY_PREFIX_DATE.length()));
		}

		// Ignore other cases silently
	}

	public boolean isBinary() {
		return options != null && options.equals(BINARY_FILE);
	}

	public boolean isAdded() {
		return revision != null && revision.equals(REVISION_ADDED);
	}

	public boolean isRemoved() {
		return revision != null && revision.startsWith(REVISION_REMOVED_PREFIX);
	}

	public boolean isValid() {
		return fileName != null && fileName.length() > 0;
	}

	public boolean isDirectory() {
		return directory;
	}

	public boolean isConflict() {
		return conflict;
	}

	public String getConflictStringWithoutConflict() {
		return conflictStringWithoutConflictMarker;
	}

	public boolean isTimeStampMatchesFile() {
		return timeStampMatchesFile;
	}

	public void setDummyTimestamp() {
		parseConflictString(DUMMY_TIMESTAMP);
	}

	/**
	 * A typical conflict string looks like "+=".
	 */
	public void parseConflictString(String conflictString) {
		setConflict(conflictString);
		this.conflictStringWithoutConflictMarker = conflictString;
		this.lastModified = null;
		this.conflict = false;
		this.timeStampMatchesFile = false;

		if (conflictString == null
				|| conflictString.equals(DUMMY_TIMESTAMP)
				|| conflictString.equals(MERGE_TIMESTAMP)
				|| conflictString.equals(DUMMY_TIMESTAMP_NEW_ENTRY)) {
			return;
		}

		int parseStartIndex = 0;
		// Look for the position of + which indicates a conflict
		final int conflictIndex = conflictStringWithoutConflictMarker.indexOf(HAD_CONFLICTS);
		if (conflictIndex >= 0) {
			conflict = true;
			parseStartIndex = conflictIndex + 1;
		}
		// if the timestamp matches the file, there will be an = following
		// the +
		final int timeMatchIndex = conflictStringWithoutConflictMarker.indexOf(TIMESTAMP_MATCHES_FILE);
		if (timeMatchIndex >= 0) {
			timeStampMatchesFile = true;
			parseStartIndex = Math.max(parseStartIndex, timeMatchIndex + 1);
		}

		// At this point the conflict index tells us where the real conflict
		// string starts
		if (parseStartIndex > 0) {
			conflictStringWithoutConflictMarker = conflictStringWithoutConflictMarker.substring(parseStartIndex);
		}

		// if we have nothing after the = then don't try to parse it
		if (conflictStringWithoutConflictMarker.length() == 0) {
			conflictStringWithoutConflictMarker = null;
			return;
		}

		if (conflictStringWithoutConflictMarker.startsWith("Initial ")) {
			return;
		}

		this.lastModified = parseLastModifiedDate(conflictStringWithoutConflictMarker);
	}

	public void addRemoved() {
		if (revision == null || !revision.startsWith(REVISION_REMOVED_PREFIX)) {
			throw new IllegalStateException("No removed entry (" + toString() + ")");
		}

		revision = revision.substring(REVISION_REMOVED_PREFIX.length());
	}

	public boolean removeAdded() {
		if (revision == null) {
			throw new IllegalStateException("No added entry (" + toString() + ")");
		}

		if (revision.equals(REVISION_ADDED)) {
			// remove entry
			return false;
		}

		revision = REVISION_REMOVED_PREFIX + revision;
		// keep entry
		return true;
	}

	// Utils ==================================================================

	private void setFileName(String fileName) {
		this.fileName = fileName;
	}

	private void setDirectory(boolean directory) {
		this.directory = directory;
	}

	private void parseLine(String entryLine) {
		// first character is a slash, so name is read from position 1
		// up to the next slash
		final int slashPosition0;
		if (entryLine.startsWith("D/")) {
			this.directory = true;
			slashPosition0 = 1;
		}
		else {
			this.directory = false;
			slashPosition0 = 0;
		}

		final int slashPosition1 = entryLine.indexOf('/', slashPosition0 + 1);
		// Test if this is a D on its own, a special case indicating that
		// directories are understood and there are no subdirectories
		// in the current folder
		if (slashPosition1 < slashPosition0 + 2) {
			return;
		}

		final int slashPosition2 = entryLine.indexOf('/', slashPosition1 + 1);
		final int slashPosition3 = entryLine.indexOf('/', slashPosition2 + 1);
		final int slashPosition4 = entryLine.indexOf('/', slashPosition3 + 1);

		// to prevent a possible StringIndexOutOfBoundsException
		if (slashPosition4 < slashPosition1) {
			return;
		}

		// note that the parameters to substring are treated as follows:
		// (inclusive, exclusive)
		this.fileName = entryLine.substring(slashPosition0 + 1, slashPosition1);
		this.revision = entryLine.substring(slashPosition1 + 1, slashPosition2);
		if (slashPosition3 - slashPosition2 > 1) {
			final String conflict = entryLine.substring(slashPosition2 + 1, slashPosition3);
			parseConflictString(conflict);
		}
		if (slashPosition4 - slashPosition3 > 1) {
			this.options = entryLine.substring(slashPosition3 + 1, slashPosition4);
		}
		if (slashPosition4 != entryLine.length() - 1) {
			final String tagOrDate = entryLine.substring(slashPosition4 + 1);
			setStickyInformation(tagOrDate);
		}
	}

	private void resetStickyInformation() {
		stickyTag = null;
		stickyRevision = null;
		stickyDateString = null;
		stickyDate = null;
	}

	private static DateFormat getLastModifiedDateFormatter() {
		if (lastModifiedDateFormatter == null) {
			lastModifiedDateFormatter = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy", Locale.US);
			lastModifiedDateFormatter.setTimeZone(TimeZoneUtils.getGMT());
		}
		return lastModifiedDateFormatter;
	}

	private static synchronized Date parseLastModifiedDate(String date) {
		try {
			return getLastModifiedDateFormatter().parse(date);
		}
		catch (ParseException ex) {
			LOG.warn("Date '" + date + "' could not be parsed", ex);
		}
		catch (NumberFormatException ex) {
			LOG.warn("Date '" + date + "' could not be parsed", ex);
		}
		return null;
	}
}

