/* DependencyTableEx.java

	Purpose:
		
	Description:
		
	History:
		Oct 24, 2014 12:47:45 PM, Created by henrichen

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

package org.zkoss.zssex.model.sys;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.zkoss.util.logging.Log;
import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SBookSeries;
import org.zkoss.zss.model.impl.sys.DependencyTableAdv;
import org.zkoss.zss.model.impl.sys.DependencyTableImpl;
import org.zkoss.zss.model.sys.dependency.ConditionalRef;
import org.zkoss.zss.model.sys.dependency.ObjectRef;
import org.zkoss.zss.model.sys.dependency.Ref;
import org.zkoss.zss.model.sys.dependency.Ref.RefType;
import org.zkoss.zss.model.sys.dependency.StyleRef;
import org.zkoss.zssex.util.Interval;

/**
 * Enhanced version with much better performance.
 * @author henri
 *
 */
public class DependencyTableEx extends DependencyTableImpl {
	private static final long serialVersionUID = -5868552271149633187L;
	private static final Log _logger = Log.lookup(DependencyTableEx.class.getName());
	
	//TODO: should get this from SpreadsheetVersion...
	private static final int MAX_ROW_LIMIT = 0x100000 - 1;
	private static final int MAX_COL_LIMIT = 0x4000 - 1;
	
	/** Map<precedent, dependant> */
	private Map<Ref, Set<Ref>> _premap = new LinkedHashMap<Ref, Set<Ref>>();
	
	/** Map<tableName, sheetTracker> */
	private Map<String, SheetTracker> _trackers = new HashMap<String, SheetTracker>();
	
	/** Map<tableName+sheetName, precedents in the inexist sheet> */
	private Map<String, Set<Ref>> _refErrorPrecedents = new HashMap<String, Set<Ref>>();
	
	@Override
	public Set<Ref> getDependents(Ref precedent) {
		LinkedHashSet<Ref> results = new LinkedHashSet<Ref>();
		getDependents0(precedent, results);
		return results;
	}
	
	private void getDependents0(Ref precedent, Set<Ref> results) {
		Set<Ref> dependents = getDirectDependents(precedent);
		for (Ref ref : dependents) {
			if (results.contains(ref)) continue; // avoid loop back(ref to self)
			handleConditionalRef(precedent, ref, results); //ZSS-1251
			results.add(ref);
			getDependents0(ref, results); //recursive
		}
	}
	
	//ZSS-1251: Special treat of the CondionalFormat ref
	private void handleConditionalRef(Ref precedent, Ref ref, Set<Ref> results) {
		if (ref instanceof ConditionalRef) {
			results.add(StyleRef.inst); // must update style
			 
		}
	}

	@Override
	public Set<Ref> getDirectDependents(Ref precedent) {
		if (_regionTypes.contains(precedent.getType())) {
			final Collection<Interval<Ref>> overlaps = overlaps(precedent);
			if (overlaps != null) {
				final LinkedHashSet<Ref> dependents = new LinkedHashSet<Ref>();
				for (Interval<Ref> ip : overlaps) {
					Ref payload = ip.getPayload();
					if (_map.containsKey(payload)) {
						dependents.add(payload);
					}
					
					final Set<Ref> refs = _premap.get(payload);
					if (refs != null) {
						dependents.addAll(refs);
					}
					
					dependents.remove(precedent);
				}
				return dependents;
			}
		} else {
			final Set<Ref> refs = _premap.get(precedent);
			if (refs != null) {
				return new LinkedHashSet<Ref>(refs); // must make a copy; we will manipulate this set later
			}
		}
		return Collections.emptySet();
	}

	@Override
	public Set<Ref> getEvaluatedDependents(Ref precedent) {
		handlePrecedentUpdate(precedent); // tricky! for sheet create/rename; ZSS-1090: extern book refence
		return getDependents(precedent);
	}

	@Override
	public void add(Ref dependant, Ref precedent) {
		// add ref into tracker
		dependant = insertRef(dependant);
		precedent = insertRef(precedent);
		
		// put into dependant precedent relation map
		super.add(dependant, precedent);
		
		//handle precedent -> dependent mapping
		Set<Ref> dependents = _premap.get(precedent);
		if(dependents == null) {
			dependents = new LinkedHashSet<Ref>();
			_premap.put(precedent, dependents);
		}
		dependents.add(dependant);
		
	}
	
	@Override
	public void setEvaluated(Ref dependent) {
		// Do nothing. No concept of evaluated map
	}

	@Override
	public void clearDependents(Ref ref) {
		Set<Ref> precedents = _map.remove(ref);
		
		if (precedents != null) {
			for (Ref precedent : precedents) {
				clearPrecedent(precedent, ref);
			}
		}
		
		_evaledMap.remove(ref);
		
		//clear from tracker
		if (_premap.get(ref) == null || _premap.get(ref).isEmpty()) {
			deleteRef(ref);
		}
	}

	//ZSS-1251
	@Override
	public void del(Ref ref, Ref precedent) {
		final Set<Ref> precedents = _map.get(ref);
		if (precedents != null) {
			precedents.remove(precedent);
			if (precedents.isEmpty()) {
				_map.remove(ref);
			}
		}
		
		clearPrecedent(precedent, ref);
		
		Set<Ref> evaled = _evaledMap.get(ref);
		if (evaled != null) {
			evaled.remove(precedent);
			if (evaled.isEmpty()) {
				_evaledMap.remove(ref);
			}
		}
		
		//clear from tracker
		if (_premap.get(ref) == null || _premap.get(ref).isEmpty()) {
			deleteRef(ref);
		}
	}

//	@Override
//	public Set<Ref> searchPrecedents(RefFilter filter) {
//		// TODO Auto-generated method stub
//		return null;
//	}
//
//	@Override
//	public void setBookSeries(SBookSeries series) {
//		
//	}
//
//	@Override
//	public Set<Ref> getDirectPrecedents(Ref dependent) {
//		
//	}
	
	@Override
	public void merge(DependencyTableAdv dependencyTable) {
		super.merge(dependencyTable);
		if(!(dependencyTable instanceof DependencyTableEx)) {
			// just in case
			_logger.error("can't merge different type of dependency table: " 
					+ dependencyTable.getClass().getName() 
					+ " to this advanced dependency table: " 
					+ this.getClass().getName());
			return;
		}

		// simply, just put everything in
		DependencyTableEx another = (DependencyTableEx)dependencyTable;
		mergeAllRefs(_premap, another._premap); //ZSS-1222
		_trackers.putAll(another._trackers);
		mergeAllRefs(_refErrorPrecedents, another._refErrorPrecedents); //ZSS-1090, ZSS-1222
	}

	@Override
	public void adjustSheetIndex(String bookName, int index, int size) {
		SheetTracker tracker = getTracker(bookName);
		if (tracker != null) {
			tracker.adjustSheetIndex(index, size);
		}
	}
	
	@Override
	public void moveSheetIndex(String bookName, int oldIndex, int newIndex) {
		SheetTracker tracker = getTracker(bookName);
		if (tracker != null) {
			tracker.moveSheetIndex(oldIndex, newIndex);
		}
	}
	
	private String getKey(String bookName, String sheetName) {
		return bookName+"]"+sheetName;
	}
	
	private Ref insertRef(Ref ref) {
		if (_regionTypes.contains(ref.getType())) {
			SheetTracker tracker = getTracker(ref);
			if (tracker != null) {
				Interval<Ref> intv = tracker.insertRef(ref);
				//20141027, henrichen: tricky! no such sheet in ref, record 
				//   this state so we can insert it back when the sheet become
				//   available.
				if (intv == null) { //no such sheet
					registerRefErrorPrecedent(ref); //ZSS-1090
					return ref;
				} else {
					return intv.getPayload();
				}
			} else { 
				//ZSS-1090, 20150731, henrichen: trickey! Reference to
				//	external book which have not been imported; record
				//	this state so we can insert it back when the book
				//	become available.
				registerRefErrorPrecedent(ref);
			}
		}
		return ref;
	}
	
	//ZSS-1090: register this state so we can insert it back when the book
	//  become available.
	private void registerRefErrorPrecedent(Ref ref) {
		final String bookName = ref.getBookName();
		final String sht1 = ref.getSheetName();
		final String sht2 = ref.getLastSheetName();
		if (sht1 != null) {
			handleRefErrorPrecedents(ref, getKey(bookName, sht1));
		}
		if (sht2 != null && !sht1.equals(sht2)) {
			handleRefErrorPrecedents(ref, getKey(bookName, sht2));
		}
	}
	
	private void handleRefErrorPrecedents(Ref ref, String key) {
		Set<Ref> precedents = _refErrorPrecedents.get(key);
		if (precedents == null) {
			precedents = new HashSet<Ref>();
			_refErrorPrecedents.put(key, precedents);
		}
		precedents.add(ref);
	}
	
	//20141027, henrichen: tricky! When a sheet become available(by create or 
	//   rename); have to insert associated refs back into the SheetTracker. 
	private void handlePrecedentUpdate(Ref precedent) {
		if (!_refErrorPrecedents.isEmpty() && _regionTypes.contains(precedent.getType())) { //ZSS-1090
			final String bookName = precedent.getBookName();
			final String sheetName = precedent.getSheetName();
			SBook book = _books.getBook(bookName);
			if (book != null) {
				final String key = getKey(bookName, sheetName);
				if (_refErrorPrecedents.containsKey(key) &&  book.getSheetIndex(sheetName) >= 0) {
					Set<Ref> precedents = _refErrorPrecedents.remove(key);
					for (Ref p : precedents) {
						if (!bookName.equals(p.getBookName())) continue;
						if (sheetName.equals(p.getSheetName()) 
							|| sheetName.equals(p.getLastSheetName())) {
							insertRef(p);
						}
					}
				}
			}
		}
	}
	
	private void deleteRef(Ref ref) {
		if (_regionTypes.contains(ref.getType())) {
			SheetTracker tracker = getTracker(ref);
			if (tracker != null) {
				tracker.deleteRef(ref);
			}
		}
	}
	
	private Collection<Interval<Ref>> overlaps(Ref ref) {
		if (_regionTypes.contains(ref.getType())) {
			SheetTracker tracker = getTracker(ref);
			if (tracker != null) {
				return tracker.overlaps(ref);
			}
		}
		return Collections.emptySet();
	}
	
	private SheetTracker getTracker(Ref ref) {
		return getTracker(ref.getBookName());
	}
	private SheetTracker getTracker(String bookName) {
		SheetTracker tracker = _trackers.get(bookName);
		if (tracker == null) {
			final SBook book = _books.getBook(bookName);
			if (book != null) { 
				_trackers.put(bookName, tracker = new SheetTracker(book, MAX_ROW_LIMIT, MAX_COL_LIMIT));
			} else {
				//ZSS-1223: Following log would cause app developer to "mis-understand" as a ZSS bug. Mark it out to avoid such issue.
				//_logger.error("Cannot find the specified book \"" + bookName + "\" in this book series: " + _trackers.keySet());
			}
		}
		return tracker;
	}
	
	private void clearPrecedent(Ref precedent, Ref ref) {
		Set<Ref> dependents = _premap.get(precedent);
		if (dependents != null) { //ZSS-1251
			dependents.remove(ref);
			if (dependents.isEmpty()) {
				_premap.remove(precedent);
	
				//clear from tracker
				if (_map.get(precedent) == null || _map.get(precedent).isEmpty()) {
					deleteRef(precedent);
				}
			}
		}
	}
}
