/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2016 TIBCO Software Inc. All rights reserved.
 * http://www.jaspersoft.com
 *
 * Unless you have purchased a commercial license agreement from Jaspersoft,
 * the following license terms apply:
 *
 * This program is part of JasperReports.
 *
 * JasperReports is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * JasperReports is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
 */
package net.sf.jasperreports.engine.export.tabulator;

import java.awt.Color;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NavigableSet;
import java.util.SortedSet;

import net.sf.jasperreports.engine.JRBoxContainer;
import net.sf.jasperreports.engine.JRLineBox;
import net.sf.jasperreports.engine.JROrigin;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPrintFrame;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.export.PrintElementIndex;
import net.sf.jasperreports.engine.export.ExporterFilter;
import net.sf.jasperreports.engine.type.BandTypeEnum;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.util.Bounds;
import net.sf.jasperreports.engine.util.JRBoxUtil;
import net.sf.jasperreports.engine.util.Pair;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class Tabulator
{
	private static final Log log = LogFactory.getLog(Tabulator.class);
	public static final String EXCEPTION_MESSAGE_KEY_DROPPING_PARENT_ERROR = "export.tabulator.dropping.parent.error";
	
	private final ExporterFilter filter;
	private final List<? extends JRPrintElement> elements;
	
	private Table mainTable;
	
	private ParentCheck parentCheck = new ParentCheck();
	private SpanRangeCheck spanRangeCheck = new SpanRangeCheck();
	private SpanCheck spanCheck = new SpanCheck();
	private CollapseCheck collapseCheck = new CollapseCheck();
	private TableCellCreator tableCellCreator = new TableCellCreator();

	public Tabulator(ExporterFilter filter, List<? extends JRPrintElement> elements)
	{
		this.filter = filter;
		this.elements = elements;
		
		this.mainTable = new Table(this);
	}

	public void tabulate()
	{
		// TODO lucianc force background as different layer
		layoutElements(elements, mainTable, null, null, 0, 0, null);
	}

	protected void layoutElements(List<? extends JRPrintElement> elementList, Table table, 
			FrameCell parentCell, PrintElementIndex parentIndex,
			int xOffset, int yOffset, Bounds elementBounds)
	{
		if (log.isTraceEnabled())
		{
			log.trace("laying out " + elements.size() + " elements for parent " + parentCell
					+ " at offsets " + xOffset + ", " + yOffset);
		}
		
		// iterating the list in reverse order so that background band elements come last
		for (ListIterator<? extends JRPrintElement> it = elementList.listIterator(elementList.size()); it.hasPrevious();)
		{
			JRPrintElement element = it.previous();
			if (filter != null && !filter.isToExport(element))
			{
				if (log.isTraceEnabled())
				{
					log.trace("element " + element.getUUID() + " skipped by filter " + element);
				}
				continue;
			}
			
			if (element.getWidth() <= 0 || element.getHeight() <= 0)
			{
				if (log.isDebugEnabled())
				{
					log.debug("element " + element.getUUID() 
							+ " skipped, size " + element.getWidth() + ", " + element.getHeight());
				}
				continue;
			}
			
			if (elementBounds != null && !elementBounds.contains(element.getX(), element.getX() + element.getWidth(), 
					element.getY(), element.getY() + element.getHeight()))
			{
				if (log.isDebugEnabled())
				{
					log.debug("element " + element.getUUID() 
							+ " at [" + element.getX() + "," + (element.getX() + element.getWidth())
							+ "),[" + element.getY() + "," + (element.getY() + element.getHeight())
							+ ") does not fit inside bounds " + elementBounds);
				}
				continue;
			}
			
			placeElement(table, parentCell, xOffset, yOffset, element, parentIndex, it.nextIndex(), true);
		}
	}
	
	protected boolean placeElement(Table table, FrameCell parentCell, 
			int xOffset, int yOffset,
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex, boolean allowOverlap)
	{
		DimensionRange<Column> colRange = table.columns.getRange(element.getX() + xOffset, 
				element.getX() + element.getWidth() + xOffset);
		DimensionRange<Row> rowRange = table.rows.getRange(element.getY() + yOffset, 
				element.getY() + element.getHeight() + yOffset);
		
		if (log.isTraceEnabled())
		{
			log.trace("placing element " + element.getUUID() + " at " + colRange.start + ", " + colRange.end
					+ ", " + rowRange.start + ", " + rowRange.end);
		}
		
		boolean overlap = false;
		Bounds overlapBounds = new Bounds(colRange.start, colRange.end, rowRange.start, rowRange.end);
		
		JROrigin elementOrigin = element.getOrigin();
		if (parentCell == null // top level element
				&& elementOrigin != null && elementOrigin.getReportName() == null
				// master background element
				// TODO lucianc do something for subreport background bands as well
				&& elementOrigin.getBandTypeValue() == BandTypeEnum.BACKGROUND)
		{
			// create a layer as big as the table for the master background band
			SortedSet<Column> userColumns = table.columns.getUserEntries();
			SortedSet<Row> userRows = table.rows.getUserEntries();
			// check if we have something in the table
			if (!userColumns.isEmpty() && !userRows.isEmpty())
			{
				overlapBounds.grow(userColumns.first().startCoord, userColumns.last().endCoord,
						userRows.first().startCoord, userRows.last().endCoord);
				// TODO lucianc avoid the following cell overlap checks
			}
		}
		
		Bounds covered = null;
		Bounds originalBounds;
		
		overlapLoop:
		do
		{
			originalBounds = overlapBounds.cloneBounds();
			
			if (rowRange.start != overlapBounds.getStartY() || rowRange.end != overlapBounds.getEndY())
			{
				rowRange = table.rows.getRange(overlapBounds.getStartY(), overlapBounds.getEndY());
			}
			if (colRange.start != overlapBounds.getStartX() || colRange.end != overlapBounds.getEndX())
			{
				colRange = table.columns.getRange(overlapBounds.getStartX(), overlapBounds.getEndX());
			}
			
			for (Row row : rowRange.rangeSet)
			{
				for (Column col : colRange.rangeSet)
				{
					if (covered != null && covered.contains(col.startCoord, col.endCoord, row.startCoord, row.endCoord))
					{
						//we've been here before
						continue;
					}
					
					Cell cell = row.getCell(col);
					if (!canOverwrite(cell, parentCell))
					{
						overlap = true;
						if (!allowOverlap)
						{
							break overlapLoop;
						}

						// TODO lucianc see if we can avoid some of these checks
						Cell overlapParentCell = overlapParentCell(cell, parentCell);
						Pair<Column, Column> colSpanRange = getColumnSpanRange(table, col, row, overlapParentCell);
						Pair<Row, Row> rowSpanRange = getRowSpanRange(table, col, row, overlapParentCell);
						
						if (log.isTraceEnabled())
						{
							log.trace("found overlap with cell " + cell 
									+ ", overlap parent " + overlapParentCell
									+ ", column span range " + colSpanRange.first().startCoord + " to " + colSpanRange.second().startCoord
									+ ", row span range " + rowSpanRange.first().startCoord + " to " + rowSpanRange.second().startCoord);
						}
						
						overlapBounds.grow(colSpanRange.first().startCoord, colSpanRange.second().startCoord, 
								rowSpanRange.first().startCoord, rowSpanRange.second().startCoord);
					}
				}
			}
			
			covered = originalBounds;
		}
		while (!originalBounds.equals(overlapBounds));
		
		if (!overlap)
		{
			setElementCells(table, parentCell, xOffset, yOffset, element, parentIndex, elementIndex, 
					colRange, rowRange);
			return true;
		}
		
		if (!allowOverlap)
		{
			return false;
		}
		
		placeOverlappedElement(table, parentCell, xOffset, yOffset, 
				element, parentIndex, elementIndex, 
				overlapBounds);
		return true;
	}

	protected void placeOverlappedElement(Table table, FrameCell parentCell, int xOffset, int yOffset, 
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex, 
			Bounds overlapBounds)
	{
		DimensionRange<Column> overlapColRange = table.columns.getRange(overlapBounds.getStartX(), overlapBounds.getEndX());
		DimensionRange<Row> overlapRowRange = table.rows.getRange(overlapBounds.getStartY(), overlapBounds.getEndY());
		DimensionRange<Column> layeredColRange = table.columns.addEntries(overlapColRange);
		DimensionRange<Row> layeredRowRange = table.rows.addEntries(overlapRowRange);
		
		// TODO lucianc expand existing layered cell if smaller than the current element
		boolean placed = false;
		Cell firstOverlapCell = overlapRowRange.floor.getCell(overlapColRange.floor);
		if (firstOverlapCell instanceof LayeredCell)
		{
			LayeredCell layeredCell = (LayeredCell) firstOverlapCell;
			// get the opposite corner cell
			Column lastCol = table.columns.getEntries().lower(layeredColRange.ceiling);
			Row lastRow = table.rows.getEntries().lower(layeredRowRange.ceiling);
			Cell lastCell = lastRow.getCell(lastCol);
			if (lastCell != null && (layeredCell.equals(lastCell) || layeredCell.accept(spanCheck, lastCell)))
			{
				placed = true;
				placeInLayeredCell(xOffset, yOffset, 
						element, parentIndex, elementIndex, 
						layeredCell, layeredColRange, layeredRowRange);
			}
		}
		
		if (!placed)
		{
			createLayeredCell(table, parentCell, xOffset, yOffset, 
					element, parentIndex, elementIndex, 
					layeredColRange, layeredRowRange);
		}
	}

	protected void placeInLayeredCell(int xOffset, int yOffset, 
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex,
			LayeredCell layeredCell, DimensionRange<Column> layeredColRange,
			DimensionRange<Row> layeredRowRange)
	{
		// attempt to place it on the first layer
		Table firstLayer = layeredCell.getLayers().get(0);
		boolean placed = placeElement(firstLayer, null, 
				xOffset - layeredColRange.start, yOffset - layeredRowRange.start, 
				element, parentIndex, elementIndex, false);
		if (placed)
		{
			if (log.isTraceEnabled())
			{
				log.trace("placed element on first layer of " + layeredCell);
			}
		}
		else
		{
			// create a new layer
			createOverlappedLayer(xOffset, yOffset, layeredCell, 
					element, parentIndex, elementIndex, 
					layeredColRange, layeredRowRange);
		}
	}

	protected void createLayeredCell(Table table, FrameCell parentCell, int xOffset, int yOffset, 
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex,
			DimensionRange<Column> layeredColRange, DimensionRange<Row> layeredRowRange)
	{
		if (log.isDebugEnabled())
		{
			log.debug("creating layered cell at " + layeredColRange + ", " + layeredRowRange);
		}
		
		LayeredCell layeredCell = new LayeredCell(parentCell);
		Table firstLayer = new Table(this);
		layeredCell.addLayer(firstLayer);
		moveCellsToLayerTable(parentCell, firstLayer, 
				layeredColRange, layeredRowRange);
		
		setElementCells(layeredColRange, layeredRowRange, layeredCell);
		
		collapseSpanColumns(table, layeredColRange);
		collapseSpanRows(table, layeredRowRange);
		
		createOverlappedLayer(xOffset, yOffset, layeredCell, 
				element, parentIndex, elementIndex, 
				layeredColRange, layeredRowRange);
	}

	protected void createOverlappedLayer(int xOffset, int yOffset, LayeredCell layeredCell, 
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex,
			DimensionRange<Column> layeredColRange, DimensionRange<Row> layeredRowRange)
	{
		Table overlappedLayer = new Table(this);
		layeredCell.addLayer(overlappedLayer);
		overlappedLayer.columns.addEntry(0);
		//adding the final range entry so that narrower layers do not stretch to 100% in HTML
		overlappedLayer.columns.addEntry(layeredColRange.end - layeredColRange.start);
		overlappedLayer.rows.addEntry(0);
		//not adding final range entry for rows for now, not needed at the moment
		
		int layerXOffset = xOffset - layeredColRange.start;
		int layerYOffset = yOffset - layeredRowRange.start;
		DimensionRange<Column> layerColRange = overlappedLayer.columns.getRange(element.getX() + layerXOffset, 
				element.getX() + element.getWidth() + layerXOffset);
		DimensionRange<Row> layerRowRange = overlappedLayer.rows.getRange(element.getY() + layerYOffset, 
				element.getY() + element.getHeight() + layerYOffset);
		setElementCells(overlappedLayer, null, layerXOffset, layerYOffset, 
				element, parentIndex, elementIndex, 
				layerColRange, layerRowRange);
	}

	protected void setElementCells(Table table, FrameCell parentCell, 
			int xOffset, int yOffset, 
			JRPrintElement element, PrintElementIndex parentIndex, int elementIndex,
			DimensionRange<Column> colRange, DimensionRange<Row> rowRange)
	{
		DimensionRange<Column> elementColRange = table.columns.addEntries(colRange);
		DimensionRange<Row> elementRowRange = table.rows.addEntries(rowRange);
		
		if (element instanceof JRPrintFrame)
		{
			JRPrintFrame frame = (JRPrintFrame) element;
			FrameCell frameCell = new FrameCell(parentCell, parentIndex, elementIndex);
			setElementCells(elementColRange, elementRowRange, frameCell);
			
			// go deep in the frame
			PrintElementIndex frameIndex = new PrintElementIndex(parentIndex, elementIndex);
			JRLineBox box = frame.getLineBox();
			layoutElements(frame.getElements(), table, frameCell, frameIndex, 
					xOffset + frame.getX() + box.getLeftPadding(), 
					yOffset + frame.getY() + box.getTopPadding(),
					new Bounds(0, frame.getWidth()  - box.getLeftPadding() - box.getRightPadding(),
							0, frame.getHeight() - box.getTopPadding() - box.getBottomPadding()));
		}
		else
		{
			ElementCell elementCell = new ElementCell(parentCell, parentIndex, elementIndex);
			setElementCells(elementColRange, elementRowRange, elementCell);
		}
	}

	protected boolean canOverwrite(Cell existingCell, FrameCell currentParent)
	{
		if (existingCell == null)
		{
			return true;
		}
		
		if (existingCell instanceof FrameCell)
		{
			return isParent((FrameCell) existingCell, currentParent);
		}
		
		return false;
	}

	protected boolean isParent(FrameCell parent, FrameCell child)
	{
		boolean foundAncestor = false;
		FrameCell ancestor = child;
		while (ancestor != null)
		{
			if (ancestor.equals(parent))
			{
				foundAncestor = true;
				break;
			}
			ancestor = ancestor.getParent();
		}
		return foundAncestor;
	}
	
	protected Cell overlapParentCell(Cell existingCell, FrameCell currentParent)
	{
		LinkedList<FrameCell> existingParents = new LinkedList<FrameCell>();
		for (FrameCell parent = existingCell.getParent(); parent != null; parent = parent.getParent())
		{
			existingParents.addFirst(parent);
		}
		
		LinkedList<FrameCell> currentParents = new LinkedList<FrameCell>();
		for (FrameCell parent = currentParent; parent != null; parent = parent.getParent())
		{
			currentParents.addFirst(parent);
		}
		
		Iterator<FrameCell> existingIt = existingParents.iterator();
		Iterator<FrameCell> currentIt = currentParents.iterator();
		while (existingIt.hasNext())
		{
			FrameCell existingParent = existingIt.next();
			FrameCell currentCell = currentIt.hasNext() ? currentIt.next() : null;
			if (currentCell == null || !existingParent.equals(currentCell))
			{
				return existingParent;
			}
		}
		
		return existingCell;
	}
	
	protected Pair<Column, Column> getColumnSpanRange(Table table, Column col, Row row, Cell spanned)
	{
		Column startCol = col;
		for (Column headCol : table.columns.getEntries().headSet(col, false).descendingSet())
		{
			Cell headCell = row.getCell(headCol);
			if (headCell == null || !spanned.accept(spanRangeCheck, headCell))
			{
				break;
			}
			startCol = headCol;
		}
		
		Column endCol = null;
		for (Column tailCol : table.columns.getEntries().tailSet(col))
		{
			endCol = tailCol;
			Cell tailCell = row.getCell(tailCol);
			if (tailCell == null || !spanned.accept(spanRangeCheck, tailCell))
			{
				break;
			}
		}
		assert endCol != null;
		assert startCol.startCoord < endCol.startCoord;
		
		return new Pair<Column, Column>(startCol, endCol);
	}
	
	protected Pair<Row, Row> getRowSpanRange(Table table, Column col, Row row, Cell spanned)
	{
		Row startRow = row;
		for (Row headRow : table.rows.getEntries().headSet(row, false).descendingSet())
		{
			Cell headCell = headRow.getCell(col);
			if (headCell == null || !spanned.accept(spanRangeCheck, headCell))
			{
				break;
			}
			startRow = headRow;
		}
		
		Row endRow = null;
		for (Row tailRow : table.rows.getEntries().tailSet(row))
		{
			endRow = tailRow;
			Cell tailCell = tailRow.getCell(col);
			if (tailCell == null || !spanned.accept(spanRangeCheck, tailCell))
			{
				break;
			}
		}
		assert endRow != null;
		assert startRow.startCoord < endRow.startCoord;
		
		return new Pair<Row, Row>(startRow, endRow);
	}

	protected void moveCellsToLayerTable(FrameCell parentCell, Table layerTable,
			DimensionRange<Column> colRange, DimensionRange<Row> rowRange)
	{
		layerTable.columns.addEntry(0, colRange.end - colRange.start);
		layerTable.rows.addEntry(0, rowRange.end - rowRange.start);

		ParentDrop parentDrop = new ParentDrop();
		for (Row row : rowRange.rangeSet)
		{
			for (Column column : colRange.rangeSet)
			{
				Cell cell = row.getCell(column);
				// drop parentCell from the cell's parent
				Cell layerCell = cell == null ? null : cell.accept(parentDrop, parentCell);
				if (layerCell != null)
				{
					// determine how much the original cell spans
					Column lastColSpan = getColumnCellSpan(colRange.rangeSet, column, row, cell).lastEntry;
					Row lastRowSpan = getRowCellSpan(rowRange.rangeSet, column, row, cell).lastEntry;
					
					// create ranges in the layer table
					DimensionRange<Column> cellColRange = layerTable.columns.getRange(
							column.startCoord - colRange.start, 
							lastColSpan.endCoord - colRange.start);
					DimensionRange<Row> cellRowRange = layerTable.rows.getRange(
							row.startCoord - rowRange.start, 
							lastRowSpan.endCoord - rowRange.start);
					
					// add entries for the ranges and set the cell in the layer table
					DimensionRange<Column> cellFinalColRange = layerTable.columns.addEntries(cellColRange);
					DimensionRange<Row> cellFinalRowRange = layerTable.rows.addEntries(cellRowRange);
					setElementCells(cellFinalColRange, cellFinalRowRange, layerCell);
				}
			}
		}
	}

	protected void collapseSpanColumns(Table table, DimensionRange<Column> range)
	{
		List<Pair<Column, Column>> removeList = new ArrayList<Pair<Column, Column>>();
		Column prevColumn = null;
		for (Column column : range.rangeSet)
		{
			if (prevColumn == null)
			{
				prevColumn = column;
				continue;
			}
			
			boolean collapse = true;
			for (Row row : table.getRows().getEntries())
			{
				Cell prevCell = row.getCell(prevColumn);
				Cell cell = row.getCell(column);
				boolean collapseCell = prevCell == null ? cell == null
						: (cell != null && prevCell.accept(collapseCheck, cell));
				if (!collapseCell)
				{
					collapse = false;
					break;
				}
			}
			
			if (collapse)
			{
				// removing outside the iteration so that the iterator is not broken
				removeList.add(new Pair<Column, Column>(column, prevColumn));
			}
			else
			{
				prevColumn = column;
			}
		}
		
		for (Pair<Column, Column> removePair : removeList)
		{
			table.removeColumn(removePair.first(), removePair.second());
		}
	}

	protected void collapseSpanRows(Table table, DimensionRange<Row> range)
	{
		List<Pair<Row, Row>> removeList = new ArrayList<Pair<Row, Row>>();
		Row prevRow = null;
		for (Row row : range.rangeSet)
		{
			if (prevRow == null)
			{
				prevRow = row;
				continue;
			}
			
			boolean collapse = true;
			for (Column column : table.getColumns().getEntries())
			{
				Cell prevCell = prevRow.getCell(column);
				Cell cell = row.getCell(column);
				boolean collapseCell = prevCell == null ? cell == null
						: (cell != null && prevCell.accept(collapseCheck, cell));
				if (!collapseCell)
				{
					collapse = false;
					break;
				}
			}
			
			if (collapse)
			{
				// removing outside the iteration so that the iterator is not broken
				removeList.add(new Pair<Row, Row>(row, prevRow));
			}
			else
			{
				prevRow = row;
			}
		}
		
		for (Pair<Row, Row> removePair : removeList)
		{
			table.removeRow(removePair.first(), removePair.second());
		}
	}

	protected void setElementCells(DimensionRange<Column> elementColRange,
			DimensionRange<Row> elementRowRange, Cell elementCell)
	{
		elementRowRange.floor.setCell(elementColRange.floor, elementCell);
		for (Column col : elementColRange.rangeSet.tailSet(elementColRange.floor, false))
		{
			elementRowRange.floor.setCell(col, elementCell.split());
		}
		
		for (Row row : elementRowRange.rangeSet.tailSet(elementRowRange.floor, false))
		{
			for (Column col : elementColRange.rangeSet)
			{
				row.setCell(col, elementCell.split());
			}
		}
	}

	public void addMargins(int width, int height)
	{
		mainTable.columns.addMargins(width);
		mainTable.rows.addMargins(height);
	}
	
	protected Column columnKey(int startCoord)
	{
		// TODO lucianc cache
		return new Column(startCoord);
	}
	
	protected Row rowKey(int startCoord)
	{
		// TODO lucianc cache
		return new Row(startCoord);
	}
	
	protected void columnSplit(Table table, Column splitCol, Column newCol)
	{
		for (Row row : table.rows.getEntries())
		{
			Cell cell = row.getCell(splitCol);
			if (cell != null)
			{
				Cell splitCell = cell.split();
				row.setCell(newCol, splitCell);
			}
		}
	}
	
	protected void rowSplit(Table table, Row splitRow, Row newRow)
	{
		for (Column col : table.columns.getEntries())
		{
			Cell cell = splitRow.getCell(col);
			if (cell != null)
			{
				Cell splitCell = cell.split();
				newRow.setCell(col, splitCell);
			}
		}
	}

	protected boolean isSplitCell(Cell spanned, Cell cell)
	{
		return (cell instanceof SplitCell) 
				&& ((SplitCell) cell).getSourceCell().equals(spanned);
	}
	
	public Table getTable()
	{
		return mainTable;
	}
	
	public JRPrintElement getCellElement(BaseElementCell cell)
	{
		return getCellElement(cell.getParentIndex(), cell.getElementIndex());
	}
	
	protected JRPrintElement getCellElement(PrintElementIndex parentIndex, int index)
	{
		// TODO lucianc keep a cache of current element position?
		JRPrintElement element;
		if (parentIndex == null)
		{
			element = elements.get(index);
		}
		else
		{
			JRPrintFrame parentFrame = (JRPrintFrame) getCellElement(parentIndex.getParentIndex(), parentIndex.getIndex());
			element = parentFrame.getElements().get(index);
		}
		return element;
	}
	
	protected boolean isParent(FrameCell parent, Cell child)
	{
		if (child == null)
		{
			return false;
		}
		
		return child.accept(parentCheck, parent);
	}

	protected SpanInfo<Column> getColumnCellSpan(TablePosition position, Cell cell)
	{
		return getColumnCellSpan(position.getTable().columns.getEntries(), 
				position.getColumn(), position.getRow(), cell);
	}
	
	protected SpanInfo<Column> getColumnCellSpan(NavigableSet<Column> columns, Column column, Row row, Cell cell)
	{
		int span = 1;
		Column lastCol = column;
		for (Column tailCol : columns.tailSet(column, false))
		{
			Cell tailCell = row.getCell(tailCol);
			if (tailCell == null || !cell.accept(spanCheck, tailCell))
			{
				break;
			}
			++span;
			lastCol = tailCol;
		}
		return new SpanInfo<Column>(span, lastCol);
	}

	protected SpanInfo<Row> getRowCellSpan(TablePosition position, Cell cell)
	{
		return getRowCellSpan(position.getTable().rows.getEntries(), 
				position.getColumn(), position.getRow(), cell);
	}

	protected SpanInfo<Row> getRowCellSpan(NavigableSet<Row> rows, Column column, Row row, Cell cell)
	{
		int span = 1;
		Row lastRow = row;
		for (Row tailRow : rows.tailSet(row, false))
		{
			Cell tailCell = tailRow.getCell(column);
			if (tailCell == null || !cell.accept(spanCheck, tailCell))
			{
				break;
			}
			++span;
			lastRow = tailRow;
		}
		return new SpanInfo<Row>(span, lastRow);
	}

	protected FrameCell droppedParent(FrameCell existingParent, FrameCell parent)
	{
		if (existingParent == null)
		{
			// should not happen
			throw 
				new JRRuntimeException(
					EXCEPTION_MESSAGE_KEY_DROPPING_PARENT_ERROR,
					(Object[])null);
		}
		
		if (existingParent.equals(parent))
		{
			return null;
		}
		
		FrameCell droppedGrandParent = droppedParent(existingParent.getParent(), parent);
		FrameCell droppedParent = new FrameCell(droppedGrandParent, 
				existingParent.getParentIndex(), existingParent.getElementIndex());
		return droppedParent;
	}
	
	public TableCell getTableCell(TablePosition position, Cell cell)
	{
		return cell.accept(tableCellCreator, position);
	}
	
	protected class ParentCheck implements CellVisitor<FrameCell, Boolean, RuntimeException>
	{
		@Override
		public Boolean visit(ElementCell cell, FrameCell parentCell)
		{
			return Tabulator.this.isParent(parentCell, cell.getParent());
		}

		@Override
		public Boolean visit(SplitCell cell, FrameCell parentCell)
		{
			return Tabulator.this.isParent(parentCell, cell.getSourceCell().getParent());
		}

		@Override
		public Boolean visit(FrameCell frameCell, FrameCell parentCell)
		{
			return Tabulator.this.isParent(parentCell, frameCell);
		}

		@Override
		public Boolean visit(LayeredCell layeredCell, FrameCell parentCell)
		{
			return Tabulator.this.isParent(parentCell, layeredCell.getParent());
		}		
	}
	
	protected class SpanRangeCheck implements CellVisitor<Cell, Boolean, RuntimeException>
	{
		@Override
		public Boolean visit(ElementCell spanned, Cell cell)
		{
			return spanned.equals(cell) || Tabulator.this.isSplitCell(spanned, cell);
		}

		@Override
		public Boolean visit(SplitCell spanned, Cell cell)
		{
			return spanned.getSourceCell().accept(this, cell);
		}

		@Override
		public Boolean visit(FrameCell spanned, Cell cell)
		{
			return Tabulator.this.isParent(spanned, cell);
		}

		@Override
		public Boolean visit(LayeredCell spanned, Cell cell)
		{
			return spanned.equals(cell) || Tabulator.this.isSplitCell(spanned, cell);
		}
	}
	
	protected class SpanCheck implements CellVisitor<Cell, Boolean, RuntimeException>
	{
		@Override
		public Boolean visit(ElementCell spanned, Cell cell)
		{
			return Tabulator.this.isSplitCell(spanned, cell);
		}

		@Override
		public Boolean visit(SplitCell spanned, Cell cell)
		{
			return false;
		}

		@Override
		public Boolean visit(FrameCell spanned, Cell cell)
		{
			return false;
		}

		@Override
		public Boolean visit(LayeredCell spanned, Cell cell)
		{
			return Tabulator.this.isSplitCell(spanned, cell);
		}
	}
	
	protected class CollapseCheck implements CellVisitor<Cell, Boolean, RuntimeException>
	{
		@Override
		public Boolean visit(ElementCell spanned, Cell cell)
		{
			return Tabulator.this.isSplitCell(spanned, cell);
		}

		@Override
		public Boolean visit(SplitCell spanned, Cell cell)
		{
			// all element/layered split cells are the same
			return spanned.equals(cell);
		}

		@Override
		public Boolean visit(FrameCell spanned, Cell cell)
		{
			// all frame split cells are the same
			return spanned.equals(cell);
		}

		@Override
		public Boolean visit(LayeredCell spanned, Cell cell)
		{
			return Tabulator.this.isSplitCell(spanned, cell);
		}
	}
	
	protected class ParentDrop implements CellVisitor<FrameCell, Cell, RuntimeException>
	{
		private final Map<FrameCell, FrameCell> parentMapping = new HashMap<FrameCell, FrameCell>();

		protected FrameCell droppedParent(FrameCell existingParent, FrameCell parent)
		{
			FrameCell droppedParent;
			if (parent == null)
			{
				droppedParent = existingParent;
			}
			else if (existingParent == null)
			{
				droppedParent = null;
			}
			else if (parentMapping.containsKey(existingParent))// we can have nulls as values
			{
				droppedParent = parentMapping.get(existingParent);
			}
			else
			{
				droppedParent = Tabulator.this.droppedParent(existingParent, parent); 
				parentMapping.put(existingParent, droppedParent);
			}
			return droppedParent;
		}
		
		@Override
		public Cell visit(ElementCell cell, FrameCell parent)
		{
			FrameCell droppedParent = droppedParent(cell.getParent(), parent);
			cell.setParent(droppedParent);
			return cell;
		}

		@Override
		public Cell visit(SplitCell cell, FrameCell parent)
		{
			// we're not explicitly moving split cells
			return null;
		}

		@Override
		public Cell visit(FrameCell frameCell, FrameCell parent)
		{
			FrameCell droppedParent = droppedParent(frameCell, parent);
			return droppedParent;
		}

		@Override
		public Cell visit(LayeredCell layeredCell, FrameCell parent)
		{
			FrameCell droppedParent = droppedParent(layeredCell.getParent(), parent);
			layeredCell.setParent(droppedParent);
			return layeredCell;
		}
	}
	
	protected class TableCellCreator implements CellVisitor<TablePosition, TableCell, RuntimeException>
	{
		@Override
		public TableCell visit(ElementCell cell, TablePosition position)
		{
			JRPrintElement element = getCellElement(cell);
			int colSpan = getColumnCellSpan(position, cell).span;
			int rowSpan = getRowCellSpan(position, cell).span;
			Color backcolor = getElementBackcolor(cell);
			
			JRLineBox elementBox = (element instanceof JRBoxContainer) ? ((JRBoxContainer) element).getLineBox() : null;
			JRLineBox box = copyParentBox(cell, element, elementBox, true, true, true, true);
			
			TableCell tableCell = new TableCell(Tabulator.this, position, cell, element, colSpan, rowSpan, backcolor, box);
			return tableCell;
		}

		@Override
		public TableCell visit(SplitCell cell, TablePosition position)
		{
			// NOP
			return null;
		}

		@Override
		public TableCell visit(FrameCell frameCell, TablePosition position)
		{
			JRPrintElement element = getCellElement(frameCell);
			Color backcolor = getElementBackcolor(frameCell);
			
			boolean[] borders = getFrameCellBorders(position.getTable(), frameCell,
					position.getColumn(), position.getColumn(),
					position.getRow(), position.getRow());
			JRLineBox box = copyFrameBox(frameCell, (JRPrintFrame) element, null, borders[0], borders[1], borders[2], borders[3]);
			
			return new TableCell(Tabulator.this, position, frameCell, element, 1, 1, backcolor, box);
		}

		@Override
		public TableCell visit(LayeredCell layeredCell, TablePosition position)
		{
			SpanInfo<Column> colSpan = getColumnCellSpan(position, layeredCell);
			SpanInfo<Row> rowSpan = getRowCellSpan(position, layeredCell);
			Color backcolor = getElementBackcolor(layeredCell.getParent());
			
			JRLineBox box = null;
			FrameCell parentCell = layeredCell.getParent();
			if (parentCell != null)
			{
				boolean[] borders = getFrameCellBorders(position.getTable(), parentCell,
						position.getColumn(), colSpan.lastEntry,
						position.getRow(), rowSpan.lastEntry);
				if (borders[0] || borders[1] || borders[2] || borders[3])
				{
					JRPrintFrame parentFrame = (JRPrintFrame) getCellElement(parentCell);
					box = copyFrameBox(parentCell, parentFrame, null, borders[0], borders[1], borders[2], borders[3]);
				}
			}
			
			return new TableCell(Tabulator.this, position, layeredCell, null, colSpan.span, rowSpan.span, backcolor, box);
		}
		
		protected Color getElementBackcolor(BaseElementCell cell)
		{
			if (cell == null)
			{
				return null;
			}
			
			JRPrintElement element = getCellElement(cell);
			if (element.getModeValue() == ModeEnum.OPAQUE)
			{
				return element.getBackcolor();
			}
			
			return getElementBackcolor(cell.getParent());
		}
		
		protected JRLineBox copyParentBox(Cell cell, JRPrintElement element, JRLineBox baseBox, 
				boolean keepLeft, boolean keepRight, boolean keepTop, boolean keepBottom)
		{
			FrameCell parentCell = cell.getParent();
			if (parentCell == null)
			{
				return baseBox;
			}
			
			// TODO lucianc check this in the table instead?
			JRPrintFrame parentFrame = (JRPrintFrame) getCellElement(parentCell);
			keepLeft &= element.getX() == 0 && parentFrame.getLineBox().getLeftPadding() == 0;
			keepRight &= (element.getX() + element.getWidth() + parentFrame.getLineBox().getLeftPadding()) == parentFrame.getWidth();
			keepTop &= element.getY() == 0 && parentFrame.getLineBox().getTopPadding() == 0;
			keepBottom &= (element.getY() + element.getHeight() + parentFrame.getLineBox().getTopPadding()) == parentFrame.getHeight();
			
			JRLineBox resultBox = baseBox;
			if (keepLeft || keepRight || keepTop || keepBottom)
			{
				resultBox = copyFrameBox(parentCell, parentFrame, baseBox, 
						keepLeft, keepRight, keepTop, keepBottom);
			}
			return resultBox;
		}
		
		protected boolean[] getFrameCellBorders(Table table, FrameCell cell,
				Column firstCol, Column lastCol,
				Row firstRow, Row lastRow)
		{
			Column prevCol = table.columns.getEntries().lower(firstCol);
			boolean leftBorder = !isParent(cell, firstRow.getCell(prevCol));
			
			Column nextCol = table.columns.getEntries().higher(lastCol);
			boolean rightBorder = !isParent(cell, firstRow.getCell(nextCol));
			
			Row prevRow = table.rows.getEntries().lower(firstRow);
			boolean topBorder = !isParent(cell, prevRow.getCell(firstCol));
			
			Row nextRow = table.rows.getEntries().higher(lastRow);
			boolean bottomBorder = !isParent(cell, nextRow.getCell(firstCol));
			
			return new boolean[]{leftBorder, rightBorder, topBorder, bottomBorder};
		}

		protected JRLineBox copyFrameBox(FrameCell frameCell, JRPrintFrame frame, JRLineBox baseBox, 
				boolean keepLeft, boolean keepRight, boolean keepTop, boolean keepBottom)
		{
			// TODO lucianc cache
			JRLineBox resultBox = JRBoxUtil.copyBordersNoPadding(frame.getLineBox(), 
					keepLeft, keepRight, keepTop, keepBottom, baseBox);
			// recurse
			resultBox = copyParentBox(frameCell, frame, resultBox, keepLeft, keepRight, keepTop, keepBottom);
			return resultBox;
		}
		
	}
	
	protected static class SpanInfo<T extends DimensionEntry>
	{
		protected final int span;
		protected final T lastEntry;
		
		public SpanInfo(int span, T lastEntry)
		{
			this.span = span;
			this.lastEntry = lastEntry;
		}
	}
}