/*******************************************************************************
 * Copyright (c) 2015, 2016 Google, 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:
 *   Stefan Xenos (Google) - Initial implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.core.nd.field;

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

import org.eclipse.jdt.internal.core.nd.ITypeFactory;
import org.eclipse.jdt.internal.core.nd.Nd;
import org.eclipse.jdt.internal.core.nd.NdNode;
import org.eclipse.jdt.internal.core.nd.db.BTree;
import org.eclipse.jdt.internal.core.nd.db.ModificationLog;
import org.eclipse.jdt.internal.core.nd.db.Database;
import org.eclipse.jdt.internal.core.nd.db.ModificationLog.Tag;
import org.eclipse.jdt.internal.core.nd.db.IBTreeComparator;
import org.eclipse.jdt.internal.core.nd.db.IBTreeVisitor;
import org.eclipse.jdt.internal.core.nd.db.IString;
import org.eclipse.jdt.internal.core.nd.db.IndexException;

/**
 * Declares a field representing a case-insensitive search tree over elements which are a subtype of NdNode.
 */
public class FieldSearchIndex<T extends NdNode> extends BaseField implements IDestructableField {
	private final ITypeFactory<BTree> btreeFactory;
	FieldSearchKey<?> searchKey;
	private final Tag destructTag;
	private static IResultRank anything = new IResultRank() {
		@Override
		public long getRank(Nd nd, long address) {
			return 1;
		}
	};
 
	public static final class SearchCriteria {
		private boolean matchCase = true;
		private boolean isPrefix = false;
		private char[] searchString;
		private short requiredNodeType = -1;
		private boolean matchingParentNodeAddress = false;

		private SearchCriteria(char[] searchString) {
			this.searchString = searchString;
		}

		public static SearchCriteria create(String searchString) {
			return create(searchString.toCharArray());
		}

		public static SearchCriteria create(char[] searchString) {
			return new SearchCriteria(searchString);
		}

		public SearchCriteria requireNodeType(short type) {
			this.requiredNodeType = type;
			return this;
		}

		public SearchCriteria allowAnyNodeType() {
			this.requiredNodeType = -1;
			return this;
		}

		public SearchCriteria matchCase(boolean match) {
			this.matchCase = match;
			return this;
		}

		public SearchCriteria prefix(boolean isPrefixSearch) {
			this.isPrefix = isPrefixSearch;
			return this;
		}
//
//		public SearchCriteria requireParentNode(long parentNameAddress) {
//			this.requiredParentNodeAddress = parentNameAddress;
//			return this;
//		}

		public boolean isMatchingParentNodeAddress() {
			return this.matchingParentNodeAddress;
		}

		public boolean isMatchingCase() {
			return this.matchCase;
		}

		public boolean isPrefixSearch() {
			return this.isPrefix;
		}

		public char[] getSearchString() {
			return this.searchString;
		}
//
//		public long getRequiredParentAddress() {
//			return this.requiredParentNodeAddress;
//		}

		public boolean acceptsNodeType(short nodeType) {
			return this.requiredNodeType == -1 || this.requiredNodeType == nodeType;
		}

		public boolean requiresSpecificNodeType() {
			return this.requiredNodeType != -1;
		}
	}

	public static interface IResultRank {
		public long getRank(Nd nd, long address);
	}

	private abstract class SearchCriteriaToBtreeVisitorAdapter implements IBTreeVisitor {
		private final SearchCriteria searchCriteria;
		private final Nd nd;

		public SearchCriteriaToBtreeVisitorAdapter(SearchCriteria searchCriteria, Nd nd) {
			this.searchCriteria = searchCriteria;
			this.nd = nd;
		}

		@Override
		public int compare(long address) throws IndexException {
			IString key = FieldSearchIndex.this.searchKey.get(this.nd, address);

			if (this.searchCriteria.isPrefixSearch()) {
				return key.comparePrefix(this.searchCriteria.getSearchString(), false);
			} else {
				return key.compareCompatibleWithIgnoreCase(this.searchCriteria.getSearchString());
			}
		}

		@Override
		public boolean visit(long address) throws IndexException {
			if (this.searchCriteria.requiresSpecificNodeType()) {
				short nodeType = NdNode.NODE_TYPE.get(this.nd, address);

				if (!this.searchCriteria.acceptsNodeType(nodeType)) {
					return true;
				}
			}

			IString key = FieldSearchIndex.this.searchKey.get(this.nd, address);

			if (this.searchCriteria.isMatchingCase()) {
				if (this.searchCriteria.isPrefixSearch()) {
					if (key.comparePrefix(this.searchCriteria.getSearchString(), true) != 0) {
						return true;
					}
				} else {
					if (key.compare(this.searchCriteria.getSearchString(), true) != 0) {
						return true;
					}
				}
			}

			return acceptResult(address);
		}

		protected abstract boolean acceptResult(long address);
	}

	private FieldSearchIndex(FieldSearchKey<?> searchKey, String structName, int fieldNumber) {
		this.btreeFactory = BTree.getFactory(new IBTreeComparator() {
			@Override
			public int compare(Nd nd, long record1, long record2) {
				IString key1 = FieldSearchIndex.this.searchKey.get(nd, record1);
				IString key2 = FieldSearchIndex.this.searchKey.get(nd, record2);

				int cmp = key1.compareCompatibleWithIgnoreCase(key2);
				if (cmp == 0) {
					cmp = Long.signum(record1 - record2);
				}

				return cmp;
			}
		});

		if (searchKey != null) {
			if (searchKey.searchIndex != null && searchKey.searchIndex != this) {
				throw new IllegalArgumentException(
					"Attempted to construct a FieldSearchIndex referring to a search key that " //$NON-NLS-1$
					+ "is already in use by a different index"); //$NON-NLS-1$
			}
			searchKey.searchIndex = this;
		}
		this.searchKey = searchKey;
		setFieldName("field " + fieldNumber + ", a " + getClass().getSimpleName() //$NON-NLS-1$//$NON-NLS-2$
				+ " in struct " + structName); //$NON-NLS-1$
		this.destructTag = ModificationLog.createTag("Destructing " + getFieldName()); //$NON-NLS-1$
	}

	public static <T extends NdNode, B> FieldSearchIndex<T> create(StructDef<B> builder,
			final FieldSearchKey<B> searchKey) {

		FieldSearchIndex<T> result = new FieldSearchIndex<T>(searchKey, builder.getStructName(), builder.getNumFields());

		builder.add(result);
		builder.addDestructableField(result);

		return result;
	}

	public BTree get(Nd nd, long address) {
		return this.btreeFactory.create(nd, address + this.offset);
	}

	@Override
	public void destruct(Nd nd, long address) {
		Database db = nd.getDB();
		db.getLog().start(this.destructTag);
		try {
			this.btreeFactory.destruct(nd, address);
		} finally {
			db.getLog().end(this.destructTag);
		}
	}

	@Override
	public int getRecordSize() {
		return this.btreeFactory.getRecordSize();
	}

	public T findFirst(final Nd nd, long address, final SearchCriteria searchCriteria) {
		return findBest(nd, address, searchCriteria, anything);
	}

	@SuppressWarnings("unchecked")
	public T findBest(final Nd nd, long address, final SearchCriteria searchCriteria, final IResultRank rankFunction) {
		final long[] resultRank = new long[1];
		final long[] result = new long[1];
		get(nd, address).accept(new SearchCriteriaToBtreeVisitorAdapter(searchCriteria, nd) {
			@Override
			protected boolean acceptResult(long resultAddress) {
				long rank = rankFunction.getRank(nd, resultAddress);
				if (rank >= resultRank[0]) {
					resultRank[0] = rank;
					result[0] = resultAddress;
				}
				return true;
			}
		});

		if (result[0] == 0) {
			return null;
		}
		return (T)NdNode.load(nd, result[0]);
	}

	public interface Visitor<T> {
		boolean visit(T toVisit);
	}

	public boolean visitAll(final Nd nd, long address, final SearchCriteria searchCriteria, final Visitor<T> visitor) {
		return get(nd, address).accept(new SearchCriteriaToBtreeVisitorAdapter(searchCriteria, nd) {
			@SuppressWarnings("unchecked")
			@Override
			protected boolean acceptResult(long resultAddress) {
				return visitor.visit((T)NdNode.load(nd, resultAddress));
			}
		});
	}

	public List<T> findAll(final Nd nd, long address, final SearchCriteria searchCriteria) {
		final List<T> result = new ArrayList<T>();
		get(nd, address).accept(new SearchCriteriaToBtreeVisitorAdapter(searchCriteria, nd) {
			@SuppressWarnings("unchecked")
			@Override
			protected boolean acceptResult(long resultAddress) {
				result.add((T)NdNode.load(nd, resultAddress));
				return true;
			}
		});

		return result;
	}

	public List<T> findAll(final Nd nd, long address, final SearchCriteria searchCriteria, final int count) {
		final List<T> result = new ArrayList<T>();
		get(nd, address).accept(new SearchCriteriaToBtreeVisitorAdapter(searchCriteria, nd) {

			int remainingCount = count;

			@SuppressWarnings("unchecked")
			@Override
			protected boolean acceptResult(long resultAddress) {
				result.add((T) NdNode.load(nd, resultAddress));
				this.remainingCount--;
				return this.remainingCount > 0;
			}
		});
		return result;
	}

	/**
	 * Returns the entire contents of the index as a single list.
	 */
	public List<T> asList(final Nd nd, long address) {
		final List<T> result = new ArrayList<T>();
		get(nd, address).accept(new IBTreeVisitor() {
			@Override
			public int compare(long record) {
				return 0;
			}

			@SuppressWarnings("unchecked")
			@Override
			public boolean visit(long resultAddress) {
				result.add((T)NdNode.load(nd, resultAddress));
				return true;
			}
		});

		return result;
	}
}
