/* SheetTracker.java

	Purpose:
		
	Description:
		
	History:
		Oct 24, 2014 2:10:51 PM, Created by henrichen

	Copyright (C) 2014 Potix Corporation. All Rights Reserved.
*/

package org.zkoss.zssex.model.sys;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.sys.dependency.Ref;
import org.zkoss.zss.model.sys.dependency.Ref.RefType;
import org.zkoss.zssex.util.Interval;
import org.zkoss.zssex.util.IntervalTree;

/**
 * Tracking Sheet references; e.g. Sheet!xxx, Sheet1:Sheet2!xxx.
 * @author henri
 *
 */
class SheetTracker implements Serializable {
	private static final long serialVersionUID = 2067138994732600811L;
	final private IntervalTree<RowTracker> _sheetIntervalTree;
	final private int _maxRowLimit; 
	final private int _maxColLimit;
	final private SBook _book;
	
	SheetTracker(SBook book, int maxRowLimit, int maxColLimit) {
		_sheetIntervalTree = new IntervalTree<RowTracker>(Integer.MAX_VALUE);
		_maxRowLimit = maxRowLimit;
		_maxColLimit = maxColLimit;
		_book = book;
	}

	Interval<Ref> insertRef(Ref ref) {
		if (ref instanceof AdjustableRef) {
			ref = ((AdjustableRef)ref).getInner();
		}
		final int[] lowhigh = prepareSheetLowHigh(ref);
		if (lowhigh == null) return null; // no such sheet
		Interval<RowTracker> interval = new Interval<RowTracker>(lowhigh[0], lowhigh[1], new RowTracker(_maxRowLimit, _maxColLimit));
		Interval<RowTracker> intv = _sheetIntervalTree.insertInterval(interval, null);
		RowTracker rowTracker = intv.getPayload();
		return rowTracker.insertRef(new AdjustableRef(intv, ref));
	}
	
	Interval<Ref> deleteRef(Ref ref) {
		final Interval<RowTracker> intv = searchRowTracker(ref);
		if (intv == null) return null;
		
		final RowTracker rowTracker = intv.getPayload();
		Interval<Ref> refIntv = rowTracker.deleteRef(ref);
		
		if (rowTracker.isEmpty()) {
			_sheetIntervalTree.deleteInterval(intv, null);
		}
		
		return refIntv;
	}
	
	Interval<Ref> searchRef(Ref ref) {
		final Interval<RowTracker> intv = searchRowTracker(ref);
		if (intv == null) return null;
		
		final RowTracker rowTracker = intv.getPayload();
		return rowTracker.searchRef(ref);
	}
	
	Collection<Interval<Ref>> overlaps(Ref ref) {
		final int[] lowhigh = ref.getType() == RefType.BOOK ?
				new int[] {0, Integer.MAX_VALUE} : prepareSheetLowHigh(ref);
		if (lowhigh == null) return null; // no such sheet
		Interval<RowTracker> interval = new Interval<RowTracker>(lowhigh[0], lowhigh[1], null);
		Collection<Interval<RowTracker>> intervals = _sheetIntervalTree.overlaps(interval);
		final LinkedHashSet<Interval<Ref>> results = new LinkedHashSet<Interval<Ref>>();
		for (Interval<RowTracker> intv : intervals) {
			final RowTracker rowTracker = intv.getPayload();
			results.addAll(rowTracker.overlaps(ref));
		}
		return results;
	}
	
//	Collection<Interval<Ref>> adjustSheet(int low, int high) {
//		Interval<RowTracker> interval = new Interval<RowTracker>(low, Integer.MAX_VALUE, null);
//		Collection<Interval<RowTracker>> intervals = _sheetIntervalTree.contains(interval);
//		final LinkedHashSet<Interval<Ref>> results = new LinkedHashSet<Interval<Ref>>();
//		final Ref rowRef = new RefImpl((AbstractBookAdv)_book); //sheet is not important
//		for (Interval<RowTracker> intv : intervals) {
//			final RowTracker rowTracker = intv.getPayload();
//			results.addAll(rowTracker.contained(rowRef));
//		}
//		
//		// delete
//		for (Interval<Ref> intv : results) { 
//			final Ref payload = intv.getPayload();
//			deleteRef(payload);
//		}
//		// modify
//		for (Interval<Ref> intv : results) {
//			final Ref ref = intv.getPayload();
//			final boolean infinite = 
//			final Ref newRef = 
//				new RefImpl(ref.getBookName(), ref.getSheetName(), ref.getLastSheetName(), ref.getRow(), ref.getColumn(), ref.getLastRow(), ref.getLastColumn());
//		}
//		return results;
//		
//	}
	
	boolean isEmpty() {
		return _sheetIntervalTree.isEmpty();
	}

	private int[] prepareSheetLowHigh(Ref ref) {
		final int low = ref instanceof AdjustableRef ? 
			ref.getSheetIndex() : ref.getSheetIndex() < 0 ? 
				_book.getSheetIndex(ref.getSheetName()) : ref.getSheetIndex();
		if (low < 0) return null; // no such sheet
		final int high = ref instanceof AdjustableRef ?
			ref.getLastSheetIndex() : _book.getSheetIndex(ref.getLastSheetName());
		return new int[] {low, high < 0 ? low : high};
	}
	
	Interval<RowTracker> searchRowTracker(Ref ref) {
		final int[] lowhigh = prepareSheetLowHigh(ref);
		if (lowhigh == null) return null; // no such sheet
		Interval<RowTracker> interval = new Interval<RowTracker>(lowhigh[0], lowhigh[1], new RowTracker(_maxRowLimit, _maxColLimit));
		return _sheetIntervalTree.searchInterval(interval);
	}
	
	//insert or delete a sheet that cause sheet index change
	void adjustSheetIndex(int index, int size) {
		final Collection<Interval<RowTracker>> results = 
				_sheetIntervalTree.overlaps(new Interval<RowTracker>(index, Integer.MAX_VALUE, null));
		
		// delete related
		// on delete, payload will be removed
		List<RowTracker> payloads = new ArrayList<RowTracker>(results.size());
		for (Interval<RowTracker> intv : results) {
			payloads.add(intv.getPayload());
			_sheetIntervalTree.deleteInterval(intv, null);
		}
		
		// modify indexes
		Set<Interval<RowTracker>> skip = new HashSet<Interval<RowTracker>>();
		for (Interval<RowTracker> intv : results) {
			int high = intv.getHigh();
			high += size;
			if (high < index) {
				skip.add(intv);
				continue;
			} else if (size > 0 && high < 0) { //overflow int limit
				high = Integer.MAX_VALUE;
			}
			intv.setHigh(high);
			
			int low = intv.getLow();
			if (low >= index) {
				low += size;
				if (size > 0 && low < 0) { //overflow int limit
					skip.add(intv);
					continue;
				}
				if (low < index) low = index;
				intv.setLow(low);
			}
		}

		// insert back
		final Iterator<RowTracker> it = payloads.iterator();
		for (Interval<RowTracker> intv : results) {
			final RowTracker payload = it.next();
			if (skip.contains(intv)) continue;
			intv.setPayload(payload);
			_sheetIntervalTree.insertInterval(intv, null);
		}
	}

	// move(delete then insert) that cause sheet index change
	void moveSheetIndex(int oldIndex, int newIndex) {
		if (oldIndex == newIndex) return;
		
		final int offset = oldIndex < newIndex ? -1 : 1;
		final int low = oldIndex < newIndex ?  oldIndex : newIndex;
		final int high = oldIndex < newIndex ? newIndex : oldIndex;
		
		final Collection<Interval<RowTracker>> results = 
				_sheetIntervalTree.overlaps(new Interval<RowTracker>(low, high, null));
		
		// delete related
		// on delete, payload will be removed
		List<RowTracker> payloads = new ArrayList<RowTracker>(results.size());
		for (Interval<RowTracker> intv : results) {
			payloads.add(intv.getPayload());
			_sheetIntervalTree.deleteInterval(intv, null);
		}
		
		// modify index
		for (Interval<RowTracker> intv : results) {
			final int low0 = intv.getLow();
			final int high0 = intv.getHigh();
			if (low0 == oldIndex) {
				if (low0 == high0) { //1. interval is contained by the moving key interval
					intv.setLow(newIndex);
					intv.setHigh(newIndex);
				} else if (newIndex >= high0) { //2. move beyond original high end
					//must change low end sheet name! (_map & _remap must be remapped)
					intv.setLow(low0 + offset);
					intv.setHigh(high0 + offset);
				}
			} else if (low0 >= low) { //3. low end affected 
				intv.setLow(low0 + offset);
			}
			
			if (high0 == oldIndex) {
				if (low0 != high0 && newIndex <= low0) { //4. move beyond original low end
					intv.setLow(low0 + offset);
					// high0 index not change but sheet name changed
				}
			} else if (high0 <= high) {
				intv.setHigh(high0 + offset);
			}
		}
			
		// insert back
		final Iterator<RowTracker> it = payloads.iterator();
		for (Interval<RowTracker> intv : results) {
			final RowTracker payload = it.next();
			intv.setPayload(payload);
			_sheetIntervalTree.insertInterval(intv, null);
		}
	}
	
	@Override
	public String toString() {
		Collection<Interval<RowTracker>> results = _sheetIntervalTree.contained(new Interval<RowTracker>(0, Integer.MAX_VALUE, null));
		return results.toString();
	}
	
	private static class AdjustableRef implements Ref, Serializable {
		private static final long serialVersionUID = -7386446308287331432L;
		protected Ref _inner;
		private Interval<RowTracker> _interval;
		
		AdjustableRef(Interval<RowTracker> interval, Ref inner) {
			_interval = interval;
			_inner = inner;
		}
		
		@Override
		public RefType getType() {
			return _inner.getType();
		}

		@Override
		public String getBookName() {
			return _inner.getBookName();
		}

		@Override
		public String getSheetName() {
			return _inner.getSheetName();
		}

		@Override
		public String getLastSheetName() {
			return _inner.getLastSheetName();
		}

		@Override
		public int getRow() {
			return _inner.getRow();
		}

		@Override
		public int getColumn() {
			return _inner.getColumn();
		}

		@Override
		public int getLastRow() {
			return _inner.getLastRow();
		}

		@Override
		public int getLastColumn() {
			return _inner.getLastColumn();
		}

		@Override
		public int getSheetIndex() {
			return _interval.getLow();
		}
		
		@Override
		public int getLastSheetIndex() {
			return _interval.getHigh();
		}

		Ref getInner() {
			return _inner;
		}
		
		@Override
		public String toString() {
			return "AdjustableRef("+getSheetIndex()+":"+getLastSheetIndex()+")"+_inner.toString();
		}
		
		@Override
		public int hashCode() {
			return _inner.hashCode();
		}
		
		@Override
		public boolean equals(Object other) {
			return _inner.equals(other);
		}
	}
}
