/*
 * Copyright (c) 2002-2004 Widespace, OU and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://solareclipse.sourceforge.net/legal/cpl-v10.html
 * 
 * Contributors:
 *     Igor Malinin - initial contribution
 * 
 * $Id: AbstractPartitioner.java,v 1.4 2006-10-21 23:13:53 pombredanne Exp $
 */

package net.sourceforge.phpeclipse.ui.text.rules;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.jface.text.Assert;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IDocumentPartitionerExtension;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.TypedRegion;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;

/**
 * Advanced partitioner which maintains partitions as views to connected
 * document. Views have own partitioners themselves. This class is designed as a
 * base for complex partitioners such as for JSP, PHP, ASP, etc. languages.
 * 
 * @author Igor Malinin
 */
public abstract class AbstractPartitioner implements IDocumentPartitioner,
		IDocumentPartitionerExtension {
	public final static boolean DEBUG = false;

	/** Partition scanner */
	protected IPartitionTokenScanner scanner;

	/** Connected document */
	protected IDocument document;

	/** Flat structure of the document */
	protected List nodes = new ArrayList();

	/** The offset at which the first changed partition starts */
	protected int regionStart;

	/** The offset at which the last changed partition ends */
	protected int regionEnd;

	public AbstractPartitioner(IPartitionTokenScanner scanner) {
		this.scanner = scanner;
	}

	protected FlatNode createNode(String type, int offset, int length) {
		if (DEBUG) {
			Assert.isTrue(offset >= 0, Integer.toString(offset));
		}
		FlatNode node = new FlatNode(type);
		node.offset = offset;
		node.length = length;
		return node;
	}

	protected void addInnerRegion(FlatNode position) {
		nodes.add(computeFlatNodeIndex(position.offset), position);
	}

	protected void removeInnerRegion(FlatNode position) {
		nodes.remove(position); // TODO: Indexed remove?
	}

	protected void deleteInnerRegion(FlatNode position) {
		nodes.remove(position); // TODO: Indexed remove?
	}

	protected void resizeInnerRegion(FlatNode position) {
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#connect(IDocument)
	 */
	public void connect(IDocument document) {
		this.document = document;

		initialize();
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#disconnect()
	 */
	public void disconnect() {
		nodes.clear();
		document = null;
	}

	/**
	 * Performs the initial partitioning of the partitioner's document.
	 */
	protected void initialize() {
		scanner.setRange(document, 0, document.getLength());
		// axelcl start
		nodes.clear();
		// axelcl end
		IToken token = scanner.nextToken();
		while (!token.isEOF()) {
			String contentType = getTokenContentType(token);

			if (isSupportedContentType(contentType)) {
				addInnerRegion(createNode(contentType,
						scanner.getTokenOffset(), scanner.getTokenLength()));
			}

			token = scanner.nextToken();
		}
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#documentAboutToBeChanged(DocumentEvent)
	 */
	public void documentAboutToBeChanged(DocumentEvent event) {
		regionStart = regionEnd = -1;
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#documentChanged(DocumentEvent)
	 */
	public boolean documentChanged(DocumentEvent event) {
		return (documentChanged2(event) != null);
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitionerExtension#documentChanged2(DocumentEvent)
	 */
	public IRegion documentChanged2(DocumentEvent event) {
		int first = fixupPartitions(event);

		FlatNode[] category = (FlatNode[]) nodes.toArray(new FlatNode[nodes
				.size()]);

		// repartition changed region

		String contentType = IDocument.DEFAULT_CONTENT_TYPE;

		int offset;

		if (first == 0) {
			offset = 0; // Bug #697414: first offset
		} else {
			offset = event.getOffset();

			FlatNode partition = category[first - 1];
			if (partition.includes(offset)) {
				offset = partition.offset;
				contentType = partition.type;
				--first;
			} else if (offset == partition.offset + partition.length) {
				offset = partition.offset;
				contentType = partition.type;
				--first;
			} else {
				offset = partition.offset + partition.length;
			}
		}

		// should not be changed since last conversion
		// category = (FlatNode[]) nodes.toArray(new FlatNode[nodes.size()]);
		if (DEBUG) {
			Assert.isTrue(offset >= 0, Integer.toString(offset));
		}
		scanner.setPartialRange(document, offset, document.getLength(),
				contentType, offset);

		int lastScannedPosition = offset;
		IToken token = scanner.nextToken();
		while (!token.isEOF()) {
			contentType = getTokenContentType(token);

			if (!isSupportedContentType(contentType)) {
				token = scanner.nextToken();
				continue;
			}

			offset = scanner.getTokenOffset();
			if (DEBUG) {
				Assert.isTrue(offset >= 0, scanner.toString());
			}
			int length = scanner.getTokenLength();

			lastScannedPosition = offset + length;

			// remove all affected positions
			while (first < category.length) {
				FlatNode p = category[first];
				if (p.offset + p.length < lastScannedPosition
						|| (p.overlapsWith(offset, length) && (!containsPosition(
								offset, length) || !contentType.equals(p.type)))) {
					removeInnerRegion(p);
					rememberRegion(p.offset, p.length);
					++first;
				} else {
					break;
				}
			}

			// if position already exists we are done
			if (containsPosition(offset, length)) {
				if (lastScannedPosition > event.getOffset()) {
					// TODO: optional repartition till end of doc
					return createRegion();
				}

				++first;
			} else {
				// insert the new type position
				addInnerRegion(createNode(contentType, offset, length));
				rememberRegion(offset, length);
			}
			// try {
			token = scanner.nextToken();
			// } catch (ArrayIndexOutOfBoundsException e) {
			// System.out.println(this.getClass().toString());
			// throw e;
			// }
		}

		// remove all positions behind lastScannedPosition
		// since there aren't any further types

		// Do not need to recalculate (lost remove events)!
		// first = computeIndexInInnerDocuments(lastScannedPosition);
		while (first < category.length) {
			FlatNode p = category[first++];
			removeInnerRegion(p);
			rememberRegion(p.offset, p.length);
		}

		return createRegion();
	}

	protected int fixupPartitions(DocumentEvent event) {
		int offset = event.getOffset();
		int length = event.getLength();
		int end = offset + length;

		// fixup flat nodes laying on change boundaries

		int first = computeFlatNodeIndex(offset);
		if (first > 0) {
			FlatNode p = (FlatNode) nodes.get(first - 1);

			int right = p.offset + p.length;
			if (offset < right) {
				// change overlaps with partition
				if (end < right) {
					// cahnge completely inside partition
					String text = event.getText();
					p.length -= length;
					if (text != null) {
						p.length += text.length();
					}
				} else {
					// cut partition at right
					int cut = p.offset + p.length - offset;
					p.length -= cut;
				}
			}
		}

		int last = computeFlatNodeIndex(end);
		if (first < last) {
			FlatNode p = (FlatNode) nodes.get(last - 1);

			int right = p.offset + p.length;
			if (end < right) {
				// cut partition at left
				int cut = end - p.offset;
				p.length -= cut;
				p.offset = offset;

				String text = event.getText();
				if (text != null) {
					p.offset += text.length();
				}

				--last;
			}
		}

		// fixup flat nodes laying afrer change

		String text = event.getText();
		if (text != null) {
			length -= text.length();
		}

		for (int i = last, size = nodes.size(); i < size; i++) {
			((FlatNode) nodes.get(i)).offset -= length;
		}

		// delete flat nodes laying completely inside change boundaries

		if (first < last) {
			do {
				deleteInnerRegion((FlatNode) nodes.get(--last));
			} while (first < last);

			rememberRegion(offset, 0);
		}

		return first;
	}

	/**
	 * Returns whether the given type is one of the legal content types.
	 * 
	 * @param contentType
	 *            the content type to check
	 * @return <code>true</code> if the content type is a legal content type
	 */
	protected boolean isSupportedContentType(String contentType) {
		/* TODO: implementation */
		// if (contentType != null) {
		// for (int i= 0; i < fLegalContentTypes.length; i++) {
		// if (fLegalContentTypes[i].equals(contentType)) {
		// return true;
		// }
		// }
		// }
		// return false;
		return (contentType != null);
	}

	/**
	 * Returns a content type encoded in the given token. If the token's data is
	 * not <code>null</code> and a string it is assumed that it is the encoded
	 * content type.
	 * 
	 * @param token
	 *            the token whose content type is to be determined
	 * @return the token's content type
	 */
	protected String getTokenContentType(IToken token) {
		Object data = token.getData();
		if (data instanceof String) {
			return (String) data;
		}

		return null;
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes()
	 */
	public String[] getLegalContentTypes() {
		// TODO: implementation
		return null;
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int)
	 */
	public String getContentType(int offset) {
		return getPartition(offset).getType();
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int)
	 */
	public ITypedRegion getPartition(int offset) {
		if (nodes.size() == 0) {
			return new TypedRegion(0, document.getLength(),
					IDocument.DEFAULT_CONTENT_TYPE);
		}

		int index = computeFlatNodeIndex(offset);
		if (index < nodes.size()) {
			FlatNode next = (FlatNode) nodes.get(index);

			if (offset == next.offset) {
				return new TypedRegion(next.offset, next.length, next.type);
			}

			if (index == 0) {
				return new TypedRegion(0, next.offset,
						IDocument.DEFAULT_CONTENT_TYPE);
			}

			FlatNode prev = (FlatNode) nodes.get(index - 1);

			if (prev.includes(offset)) {
				return new TypedRegion(prev.offset, prev.length, prev.type);
			}

			int end = prev.offset + prev.length;
			return new TypedRegion(end, next.offset - end,
					IDocument.DEFAULT_CONTENT_TYPE);
		}

		FlatNode prev = (FlatNode) nodes.get(nodes.size() - 1);

		if (prev.includes(offset)) {
			return new TypedRegion(prev.offset, prev.length, prev.type);
		}

		int end = prev.offset + prev.length;

		return new TypedRegion(end, document.getLength() - end,
				IDocument.DEFAULT_CONTENT_TYPE);
	}

	/*
	 * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int,
	 *      int)
	 */
	public ITypedRegion[] computePartitioning(int offset, int length) {
		List list = new ArrayList();

		int end = offset + length;

		int index = computeFlatNodeIndex(offset);
		while (true) {
			FlatNode prev = (index > 0) ? (FlatNode) nodes.get(index - 1)
					: null;

			if (prev != null) {
				if (prev.overlapsWith(offset, length)) {
					list.add(new TypedRegion(prev.offset, prev.length,
							prev.type));
				}

				if (end <= prev.offset + prev.length) {
					break;
				}
			}

			FlatNode next = (index < nodes.size()) ? (FlatNode) nodes
					.get(index) : null;

			if (next == null || offset < next.offset) {
				int off0 = offset;
				int off1 = offset + length;

				if (prev != null && off0 < prev.offset + prev.length) {
					off0 = prev.offset + prev.length;
				}

				if (next != null && next.offset < off1) {
					off1 = next.offset;
				}

				if (off0 < off1) {
					list.add(new TypedRegion(off0, off1 - off0,
							IDocument.DEFAULT_CONTENT_TYPE));
				}
			}

			if (next == null) {
				break;
			}

			++index;
		}

		return (TypedRegion[]) list.toArray(new TypedRegion[list.size()]);
	}

	/**
	 * Computes the index in the list of flat nodes at which an flat node with
	 * the given offset would be inserted. The flat node is supposed to become
	 * the first in this list of all flat nodes with the same offset.
	 * 
	 * @param offset
	 *            the offset for which the index is computed
	 * @return the computed index
	 */
	protected int computeFlatNodeIndex(int offset) {
		if (nodes.size() == 0) {
			return 0;
		}

		int left = 0, mid = 0;
		int right = nodes.size() - 1;

		FlatNode p = null;

		while (left < right) {
			mid = (left + right) / 2;

			p = (FlatNode) nodes.get(mid);

			if (offset < p.offset) {
				right = (left == mid) ? left : mid - 1;
			} else if (offset > p.offset) {
				left = (right == mid) ? right : mid + 1;
			} else if (offset == p.offset) {
				left = right = mid;
			}
		}

		int pos = left;
		p = (FlatNode) nodes.get(pos);
		if (offset > p.offset) {
			// append to the end
			pos++;
		} else {
			// entry will became the first of all entries with the same offset
			do {
				--pos;
				if (pos < 0) {
					break;
				}
				p = (FlatNode) nodes.get(pos);
			} while (offset == p.offset);
			++pos;
		}

		return pos;
	}

	public boolean containsPosition(int offset, int length) {
		int size = nodes.size();
		if (size == 0) {
			return false;
		}

		int index = computeFlatNodeIndex(offset);
		if (index < size) {
			FlatNode p = (FlatNode) nodes.get(index);

			while (p.offset == offset) {
				if (p.length == length) {
					return true;
				}

				if (++index < size) {
					p = (FlatNode) nodes.get(index);
				} else {
					break;
				}
			}
		}

		return false;
	}

	/**
	 * Helper method for tracking the minimal region containg all partition
	 * changes. If <code>offset</code> is smaller than the remembered offset,
	 * <code>offset</code> will from now on be remembered. If
	 * <code>offset + length</code> is greater than the remembered end offset,
	 * it will be remembered from now on.
	 * 
	 * @param offset
	 *            the offset
	 * @param length
	 *            the length
	 */
	protected final void rememberRegion(int offset, int length) {
		// remember start offset
		if (regionStart == -1) {
			regionStart = offset;
		} else if (offset < regionStart) {
			regionStart = offset;
		}

		// remember end offset
		int endOffset = offset + length;

		if (regionEnd == -1) {
			regionEnd = endOffset;
		} else if (endOffset > regionEnd) {
			regionEnd = endOffset;
		}
	}

	/**
	 * Creates the minimal region containing all partition changes using the
	 * remembered offsets.
	 * 
	 * @return the minimal region containing all the partition changes
	 */
	protected final IRegion createRegion() {
		if (regionStart == -1 || regionEnd == -1) {
			return null;
		}

		return new Region(regionStart, regionEnd - regionStart);
	}
}