/* ZSSFormulaEngine.java

	Purpose:
		
	Description:
		
	History:
		Dec 26, 2013 Created by Pao Wang

Copyright (C) 2013 Potix Corporation. All Rights Reserved.
 */
package org.zkoss.zssex.formula;

import java.util.Locale;

import org.zkoss.poi.ss.formula.EvaluationWorkbook;
import org.zkoss.poi.ss.formula.FormulaParser;
import org.zkoss.poi.ss.formula.FormulaParsingWorkbook;
import org.zkoss.poi.ss.formula.FormulaRenderer;
import org.zkoss.poi.ss.formula.FormulaType;
import org.zkoss.poi.ss.formula.IStabilityClassifier;
import org.zkoss.poi.ss.formula.WorkbookEvaluator;
import org.zkoss.poi.ss.formula.eval.ValueEval;
import org.zkoss.poi.ss.formula.ptg.Ptg;
import org.zkoss.poi.ss.formula.ptg.TablePtg;
import org.zkoss.poi.ss.formula.udf.UDFFinder;
import org.zkoss.poi.ss.usermodel.ZssContext;
import org.zkoss.xel.XelContext;
import org.zkoss.zss.model.SBook;
import org.zkoss.zss.model.SCell;
import org.zkoss.zss.model.SSheet;
import org.zkoss.zss.model.STable;
import org.zkoss.zss.model.SheetRegion;
import org.zkoss.zss.model.impl.AbstractBookSeriesAdv;
import org.zkoss.zss.model.impl.sys.DependencyTableAdv;
import org.zkoss.zss.model.impl.sys.formula.FormulaEngineImpl;
import org.zkoss.zss.model.impl.sys.formula.ParsingBook;
import org.zkoss.zss.model.sys.dependency.Ref;
import org.zkoss.zss.model.sys.dependency.Ref.RefType;
import org.zkoss.zss.model.sys.formula.FormulaEngine;
import org.zkoss.zss.model.sys.formula.FormulaExpression;
import org.zkoss.zss.model.sys.formula.FormulaParseContext;

/**
 * A implementation of Formula Engine with XEL context.
 * Let formula engine support user defined function, EL variable evaluation ant so on.
 * @author Pao
 */
public class FormulaEngineEx extends FormulaEngineImpl implements FormulaEngine {

	public FormulaEngineEx() {
	}

	protected Object getXelContext() {
		return XelContextHolder.getXelContext();
	}

	protected void setXelContext(Object ctx) {
		XelContextHolder.setXelContext((XelContext)ctx);
	}
	
	//ZSS-565: enforce rendering with US internal locale
	@Override
	protected String renderFormula(ParsingBook parsingBook, String formula, Ptg[] tokens, boolean always) {
		ZssContext old = ZssContext.getThreadLocal();
		try {
			ZssContext zssContext = old == null ? 
					new ZssContext(Locale.US,-1) : 
						new ZssContext(Locale.US, old.getTwoDigitYearUpperBound());
			ZssContext.setThreadLocal(zssContext);
			return super.renderFormula(parsingBook, formula, tokens, always);
		} finally {
			ZssContext.setThreadLocal(old);
		}
	}
	
	//ZSS-565: parse with locale passed in via context
	@Override
	protected Ptg[] parse(String formula, FormulaParsingWorkbook book, int sheetIndex, FormulaParseContext context) {
		final SCell cell = context.getCell();
		return FormulaParser.parse(formula, book, FormulaType.CELL, sheetIndex, context.getLocale(), 
				cell == null ? -1 : cell.getRowIndex(), cell == null ? -1 : cell.getColumnIndex()); // current sheet index in parsing is always 0
	}

	//ZSS-742
	@Override
	public FormulaExpression movePtgs(FormulaExpression fe, final SheetRegion region, final int rowOffset, final int columnOffset, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getMoveAdjuster(region, rowOffset, columnOffset);
		return adjustPtgs(fe, context, shiftAdjuster);
	}
	
	//ZSS-747
	@Override
	public FormulaExpression shrinkPtgs(FormulaExpression fe, SheetRegion srcRegion, boolean horizontal, FormulaParseContext context) {
		SSheet sheet = srcRegion.getSheet();

		// shrinking is equals to move the neighbor region
		// calculate the neighbor and offset
		int rowOffset = 0, colOffset = 0;
		SheetRegion neighbor;
		if(horizontal) {
			// adjust on column
			colOffset = -srcRegion.getColumnCount();
			int col = srcRegion.getLastColumn() + 1;
			int lastCol = sheet.getBook().getMaxColumnIndex();
			// no change on row
			int row = srcRegion.getRow();
			int lastRow = srcRegion.getLastRow();
			neighbor = new SheetRegion(sheet, row, col, lastRow, lastCol);
		} else { // vertical
			// adjust on row
			rowOffset = -srcRegion.getRowCount();
			int row = srcRegion.getLastRow() + 1;
			int lastRow = sheet.getBook().getMaxRowIndex();
			// no change on column
			int col = srcRegion.getColumn();
			int lastCol = srcRegion.getLastColumn();
			neighbor = new SheetRegion(sheet, row, col, lastRow, lastCol);
		}

		// move it
		return movePtgs(fe, neighbor, rowOffset, colOffset, context);
	}

	//ZSS-747
	@Override
	public FormulaExpression extendPtgs(FormulaExpression fe, SheetRegion srcRegion, boolean horizontal, FormulaParseContext context) {
		SSheet sheet = srcRegion.getSheet();

		// extending is equals to move selected region and neighbor region at the same time
		// calculate the target region (combined selected and neighbor) and offset
		int rowOffset = 0, colOffset = 0;
		SheetRegion neighbor;
		if(horizontal) {
			// adjust on column
			colOffset = srcRegion.getColumnCount();
			int col = srcRegion.getColumn();
			int lastCol = sheet.getBook().getMaxColumnIndex();
			// no change on row
			int row = srcRegion.getRow();
			int lastRow = srcRegion.getLastRow();
			neighbor = new SheetRegion(sheet, row, col, lastRow, lastCol);
		} else { // vertical
			// adjust on row
			rowOffset = srcRegion.getRowCount();
			int row = srcRegion.getRow();
			int lastRow = sheet.getBook().getMaxRowIndex();
			// no change on column
			int col = srcRegion.getColumn();
			int lastCol = srcRegion.getLastColumn();
			neighbor = new SheetRegion(sheet, row, col, lastRow, lastCol);
		}

		// move it
		return movePtgs(fe, neighbor, rowOffset, colOffset, context);
	}

	//ZSS-747
	@Override
	public FormulaExpression shiftPtgs(FormulaExpression fe, final int rowOffset, final int columnOffset, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getShiftAdjuster(rowOffset, columnOffset); 		
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	//ZSS-747
	@Override
	public FormulaExpression transposePtgs(FormulaExpression fe, final int rowOrigin, final int columnOrigin, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getTransposeAdjuster(rowOrigin, columnOrigin);
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	
	//ZSS-747
	@Override
	public FormulaExpression renameSheetPtgs(FormulaExpression fe, final SBook targetBook, final String oldSheetName, final String newSheetName, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getRenameSheetAdjuster(targetBook, oldSheetName, newSheetName);
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	//ZSS-747
	@Override
	public FormulaExpression renameNamePtgs(FormulaExpression fe, final SBook targetBook, final int sheetIndex, final String oldName, final String newName, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getRenameNameAdjuster(sheetIndex, oldName, newName);
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	//ZSS-747
	/**
	 * adjust Ptg[] through specific adjuster
	 * @author henri
	 * @since 3.6.0
	 */
	private FormulaExpression adjustPtgs(FormulaExpression fe, FormulaParseContext context, FormulaAdjuster adjuster) {
		if (fe.hasError()) {
			return fe;
		}
		
		// adapt and parse
		SBook book = context.getBook();
		ParsingBook parsingBook = new ParsingBook(book);
		String sheetName = context.getSheetName();
		int contextSheetIndex = parsingBook.getExternalSheetIndex(null, sheetName); // create index according parsing book logic

		Ptg[] tokens = fe.getPtgs();
		// adjust ptgs
		boolean modified = adjuster.process(contextSheetIndex, tokens, parsingBook, context);
		
		Ref singleRef = tokens.length == 1 ? toDependRef(context, parsingBook, tokens[0], 0) : null;
		Ref[] refs = singleRef==null ? null :
			(singleRef.getType() == RefType.AREA || singleRef.getType() == RefType.CELL ?new Ref[]{singleRef}:null);
		
		if (modified) {
			final String formula = renderFormula(parsingBook, null, tokens, true);
			return new FormulaExpressionImpl(formula, tokens, refs, false, null, fe.isMultipleAreaFormula());
		}
		return fe;
	}

	//ZSS-759
	@Override
	protected ValueEval evaluateFormulaExpression(WorkbookEvaluator evaluator, int sheetIndex, FormulaExpression expr, boolean ignoreDereference, Ref ref) {
		return evaluator.evaluatePtgs(sheetIndex, expr.getPtgs(), ignoreDereference, ref);
	}
	
	//ZSS-966
	@Override
	public FormulaExpression renameTableNameTablePtgs(FormulaExpression fe, final SBook book, final String oldName, final String newName, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getRenameTableAdjuster(oldName, newName);
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	//ZSS-966
	protected FormulaAdjuster getRenameTableAdjuster(final String oldName, final String newName) {
		return new FormulaAdjuster() {
			@Override
			public boolean process(int contextSheetIndex, Ptg[] tokens, ParsingBook parsingBook, FormulaParseContext context) {
				for (Ptg ptg : tokens) {
					if (ptg instanceof TablePtg) {
						final TablePtg tbPtg = (TablePtg) ptg;
						if (tbPtg.getTableName().equals(oldName))
							tbPtg.setTableName(newName);
					}
				}
				return true;
			}
		};
	}

	//ZSS-967
	@Override
	public FormulaExpression renameColumnNameTablePtgs(FormulaExpression fe, final STable table, final String oldName, final String newName, FormulaParseContext context) {
		final FormulaAdjuster shiftAdjuster = getRenameColumnAdjuster(table.getName(), oldName, newName);
		return adjustPtgs(fe, context, shiftAdjuster);
	}

	//ZSS-967
	protected FormulaAdjuster getRenameColumnAdjuster(final String tableName, final String oldName, final String newName) {
		return new FormulaAdjuster() {
			@Override
			public boolean process(int contextSheetIndex, Ptg[] tokens, ParsingBook parsingBook, FormulaParseContext context) {
				for (Ptg ptg : tokens) {
					if (ptg instanceof TablePtg) {
						final TablePtg tbPtg = (TablePtg) ptg;
						if (tbPtg.getTableName().equalsIgnoreCase(tableName)) {
							final String col1 = tbPtg.getColumn1();
							if (col1 != null) {
								if (col1.equalsIgnoreCase(oldName)) {
									tbPtg.setColumn1(newName);
									continue; // no way two same column names
								}
								final String col2 = tbPtg.getColumn2();
								if (col2 != null) {
									if (col2.equalsIgnoreCase(oldName)) {
										tbPtg.setColumn2(newName);
									}
								}
							}
						}
					}
				}
				return true;
			}
		};
	}
}
