/*
 * 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.components.table.fill;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import net.sf.jasperreports.components.headertoolbar.HeaderToolbarElement;
import net.sf.jasperreports.components.table.BaseColumn;
import net.sf.jasperreports.components.table.Column;
import net.sf.jasperreports.components.table.ColumnGroup;
import net.sf.jasperreports.components.table.ColumnVisitor;
import net.sf.jasperreports.components.table.TableComponent;
import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRLineBox;
import net.sf.jasperreports.engine.JROrigin;
import net.sf.jasperreports.engine.JRPrintElement;
import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.JRPropertyExpression;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRStyle;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.component.BaseFillComponent;
import net.sf.jasperreports.engine.component.FillPrepareResult;
import net.sf.jasperreports.engine.design.JRAbstractCompiler;
import net.sf.jasperreports.engine.design.JRReportCompileData;
import net.sf.jasperreports.engine.export.JRPdfExporterTagHelper;
import net.sf.jasperreports.engine.fill.JRFillCloneFactory;
import net.sf.jasperreports.engine.fill.JRFillComponentElement;
import net.sf.jasperreports.engine.fill.JRFillContext;
import net.sf.jasperreports.engine.fill.JRFillDatasetRun;
import net.sf.jasperreports.engine.fill.JRFillObjectFactory;
import net.sf.jasperreports.engine.fill.JRTemplateFrame;
import net.sf.jasperreports.engine.fill.JRTemplatePrintFrame;
import net.sf.jasperreports.engine.util.JRReportUtils;
import net.sf.jasperreports.engine.xml.JRXmlWriter;

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

/**
 * 
 * 
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class FillTable extends BaseFillComponent
{

	private static final Log log = LogFactory.getLog(FillTable.class);
	
	protected static final String FILL_CACHE_KEY_TABLE_INSTANCE_COUNTER = FillTable.class.getName() + "#instanceCounter";
	protected static final String EXCEPTION_MESSAGE_KEY_UNSUPPORTED_REPORT_DATA_TYPE = "components.table.unsupported.report.data.type";
	
	private final TableComponent table;
	private final JRFillObjectFactory factory;
	private Map<List<FillColumn>, FillTableSubreportFactory> fillSubreportFactories;
	private FillTableSubreport fillSubreport;
	
	private boolean filling;
	private List<FillColumn> fillColumns;
	private int fillWidth;
	private Map<JRStyle, JRTemplateFrame> printFrameTemplates = new HashMap<JRStyle, JRTemplateFrame>();

	public FillTable(TableComponent table, JRFillObjectFactory factory)
	{
		this.table = table;
		this.factory = factory;
		
		// we need to do this for return values with derived variables
		JRFillDatasetRun fillDatasetRun = factory.getDatasetRun(table.getDatasetRun());
		// this is needed for returned variables with evaluationTime=Auto
		factory.registerDatasetRun(fillDatasetRun);
		
		this.fillSubreportFactories = new HashMap<List<FillColumn>, FillTableSubreportFactory>();
	}

	public FillTable(FillTable table, JRFillCloneFactory factory)
	{
		super(table, factory);
		
		this.table = table.table;
		this.factory = table.factory;
		
		this.fillSubreportFactories = table.fillSubreportFactories;
		this.printFrameTemplates = table.printFrameTemplates;
	}

	@Override
	public void evaluate(byte evaluation) throws JRException
	{
		if (filling)
		{
			log.warn("Table fill did not complete, canceling previous table subreport");
			fillSubreport.cancelSubreportFill();
		}
		
		filling = false;
		
		evaluateColumns(evaluation);
		if (!fillColumns.isEmpty())
		{
			createFillSubreport();
			setTableInstanceCounter();
			fillSubreport.evaluateSubreport(evaluation);
		}
	}
	
	protected void setTableInstanceCounter()
	{
		JRFillContext fillerContext = fillContext.getFiller().getFillContext();
		AtomicInteger counter = (AtomicInteger) fillerContext.getFillCache(FILL_CACHE_KEY_TABLE_INSTANCE_COUNTER);
		if (counter == null)
		{
			// we just need a mutable integer, there's no actual concurrency here
			counter = new AtomicInteger();
			fillerContext.setFillCache(FILL_CACHE_KEY_TABLE_INSTANCE_COUNTER, counter);
		}
		
		int instanceIndex = counter.getAndIncrement();
		if (log.isDebugEnabled())
		{
			log.debug("table instance index is " + instanceIndex);
		}
		
		fillSubreport.getTableReport().getBaseReport().setTableInstanceIndex(instanceIndex);
	}

	protected boolean toPrintColumn(BaseColumn column, byte evaluation) throws JRException
	{
		boolean toPrint;
		JRExpression printWhenExpression = column.getPrintWhenExpression();
		if (printWhenExpression == null)
		{
			toPrint = true;
		}
		else
		{
			Boolean printWhenVal = (Boolean) evaluateExpression(
					printWhenExpression, evaluation);
			if (printWhenVal == null)
			{
				toPrint = false;
			}
			else
			{
				toPrint = printWhenVal.booleanValue();
			}
		}
		return toPrint;
	}
	
	protected JRPropertiesMap evaluateProperties(BaseColumn column, byte evaluation) throws JRException
	{
		JRPropertiesMap staticProperties = column.hasProperties() ? column.getPropertiesMap().cloneProperties() : null;
		JRPropertiesMap mergedProperties = null;

		JRPropertyExpression[] propExprs = column.getPropertyExpressions();
		if (propExprs == null || propExprs.length == 0)
		{
			mergedProperties = staticProperties;
		}
		else
		{
			JRPropertiesMap dynamicProperties = new JRPropertiesMap();
			
			for (int i = 0; i < propExprs.length; i++)
			{
				JRPropertyExpression prop = propExprs[i];
				String value = (String) evaluateExpression(prop.getValueExpression(), evaluation);
				//if (value != null) //for some properties such as data properties in metadata exporters, the null value is significant
				{
					dynamicProperties.setProperty(prop.getName(), value);
				}
			}
			
			mergedProperties = dynamicProperties.cloneProperties();
			mergedProperties.setBaseProperties(staticProperties);
		}
		
		return mergedProperties;
	}
	
	protected class FillColumnEvaluator implements ColumnVisitor<FillColumn>
	{
		final byte evaluation;
		
		public FillColumnEvaluator(byte evaluation)
		{
			this.evaluation = evaluation;
		}

		@Override
		public FillColumn visitColumn(Column column)
		{
			try
			{
				boolean toPrint = toPrintColumn(column, evaluation);
				if (toPrint)
				{
					JRPropertiesMap properties = evaluateProperties(column, evaluation);
					return new FillColumn(column, properties); 
				}
				return null;
			}
			catch (JRException e)
			{
				throw new JRRuntimeException(e);
			}
		}

		@Override
		public FillColumn visitColumnGroup(ColumnGroup columnGroup)
		{
			try
			{
				boolean toPrint = toPrintColumn(columnGroup, evaluation);
				FillColumn fillColumn;
				if (toPrint)
				{
					List<BaseColumn> columns = columnGroup.getColumns();
					List<FillColumn> subColumns = new ArrayList<FillColumn>(columns.size());
					int printWidth = 0;
					for (BaseColumn column : columns)
					{
						FillColumn fillSubColumn = column.visitColumn(this);
						if (fillSubColumn != null)
						{
							printWidth += fillSubColumn.getWidth();
							subColumns.add(fillSubColumn);
						}
					}
					
					if (subColumns.isEmpty())
					{
						// no sub columns prints
						// the column group won't print either
						fillColumn = null;
					}
					else
					{
						JRPropertiesMap properties = evaluateProperties(columnGroup, evaluation);
						fillColumn = new FillColumn(columnGroup, printWidth, subColumns, properties);
					}
				}
				else
				{
					fillColumn = null;
				}
				return fillColumn;
			}
			catch (JRException e)
			{
				throw new JRRuntimeException(e);
			}
		}
	}
	
	protected void evaluateColumns(byte evaluation)
	{
		FillColumnEvaluator columnEvaluator = new FillColumnEvaluator(evaluation);
		List<BaseColumn> columns = table.getColumns();
		fillColumns = new ArrayList<FillColumn>(columns.size());
		fillWidth = 0;
		for (BaseColumn column : columns)
		{
			FillColumn fillColumn = column.visitColumn(columnEvaluator);
			if (fillColumn != null)
			{
				fillColumns.add(fillColumn);
				fillWidth += fillColumn.getWidth();
			}
		}
	}

	protected void createFillSubreport() throws JRException
	{
		FillTableSubreportFactory subreportFactory = fillSubreportFactories.get(fillColumns);
		if (subreportFactory == null)
		{
			subreportFactory = createFillTableSubreportFactory();
			fillSubreportFactories.put(fillColumns, subreportFactory);
		}
		
		fillSubreport = subreportFactory.createFillSubreport();
	}

	protected FillTableSubreportFactory createFillTableSubreportFactory() throws JRException
	{
		JasperReport parentReport = fillContext.getFiller().getJasperReport();
		JasperReport containingReport = containingReport(parentReport);
		JRDataset reportSubdataset = JRReportUtils.findSubdataset(table.getDatasetRun(), 
				containingReport);
		
		BuiltinExpressionEvaluatorFactory builtinEvaluatorFactory = new BuiltinExpressionEvaluatorFactory();
		
		String tableReportName = JRAbstractCompiler.getUnitName(containingReport, reportSubdataset);
		
		// clone the table subdataset in order to have a different instance for other
		// elements that might be using it.
		// we're cloning the subdataset via an object factory in order to preserve
		// the relationship between objects (e.g. variables and groups) in the cloned
		// dataset
		JRDataset tableSubdataset = DatasetCloneObjectFactory.cloneDataset(reportSubdataset);
		TableReportDataset reportDataset = new TableReportDataset(tableSubdataset, tableReportName);

		TableReport tableReport = new TableReport(fillContext, table, reportDataset, fillColumns, builtinEvaluatorFactory);
		
		if (log.isDebugEnabled())
		{
			String tableReportXml = new JRXmlWriter(fillContext.getFiller().getJasperReportsContext()).write(tableReport, "UTF-8");
			log.debug("Generated table report:\n" + tableReportXml);
		}
		
		JRReportCompileData tableReportCompileData = createTableReportCompileData(
				containingReport, reportSubdataset);
		
		TableJasperReport compiledTableReport = new TableJasperReport(parentReport, tableReport, 
				tableReportCompileData, 
				new TableReportBaseObjectFactory(reportDataset),
				"");// no suffix as already included in the report name
		
		TableSubreport subreport = 
			new TableSubreport(
				table.getDatasetRun(), 
				((JRFillComponentElement)fillContext.getComponentElement()).getParent()
				);
		return 
			new FillTableSubreportFactory(
				subreport, compiledTableReport,
				builtinEvaluatorFactory
				);
	}

	protected JasperReport containingReport(JasperReport parentReport)
	{
		JasperReport containingReport = parentReport;
		while (containingReport instanceof TableJasperReport)
		{
			containingReport = ((TableJasperReport) containingReport).getParentReport();
		}
		return containingReport;
	}
	
	protected JRReportCompileData createTableReportCompileData(
			JasperReport parentReport, JRDataset reportSubdataset)
			throws JRException
	{
		Serializable reportCompileDataObj = parentReport.getCompileData();
		if (!(reportCompileDataObj instanceof JRReportCompileData))
		{
			throw 
				new JRRuntimeException(
					EXCEPTION_MESSAGE_KEY_UNSUPPORTED_REPORT_DATA_TYPE,  
					new Object[]{reportCompileDataObj.getClass().getName()} 
					);
		}
		
		JRReportCompileData reportCompileData = (JRReportCompileData) reportCompileDataObj;
		Serializable datasetCompileData = reportCompileData.getDatasetCompileData(
				reportSubdataset);
		
		TableReportCompileData tableReportCompileData = new TableReportCompileData(
				parentReport);
		tableReportCompileData.setMainDatasetCompileData(datasetCompileData);
		
		JRDataset[] datasets = parentReport.getDatasets();
		if (datasets != null)
		{
			for (JRDataset dataset : datasets)
			{
				Serializable compileData = reportCompileData.getDatasetCompileData(dataset);
				tableReportCompileData.setDatasetCompileData(dataset, compileData);
			}
		}
		tableReportCompileData.copyCrosstabCompileData(reportCompileData);
		return tableReportCompileData;
	}
	
	@Override
	public FillPrepareResult prepare(int availableHeight)
	{
		try
		{
			if (fillColumns.isEmpty())
			{
				//no columns to print
				return FillPrepareResult.NO_PRINT_NO_OVERFLOW;
			}
			
			JRTemplatePrintFrame printFrame = new JRTemplatePrintFrame(getFrameTemplate(), printElementOriginator);
			JRLineBox lineBox = printFrame.getLineBox();
			int verticalPadding = lineBox.getTopPadding() + lineBox.getBottomPadding();
			
			FillPrepareResult result = 
				fillSubreport.prepareSubreport(
					availableHeight - verticalPadding, 
					filling
					);
			
			if (verticalPadding != 0)
			{
				result = result.addStretch(verticalPadding);
			}
			
			filling = result.willOverflow();
			return result;
		}
		catch (JRException e)
		{
			throw new JRRuntimeException(e);
		}
	}

	@Override
	public JRPrintElement fill()
	{
		JRTemplatePrintFrame printFrame = new JRTemplatePrintFrame(getFrameTemplate(), printElementOriginator);
		printFrame.getPropertiesMap().setProperty(HeaderToolbarElement.PROPERTY_TABLE_UUID, fillContext.getComponentElement().getUUID().toString());

		JRLineBox lineBox = printFrame.getLineBox();
		
		printFrame.setUUID(fillContext.getComponentElement().getUUID());
		printFrame.setX(fillContext.getComponentElement().getX());
		printFrame.setY(fillContext.getElementPrintY());
		printFrame.setWidth(fillWidth + lineBox.getLeftPadding() + lineBox.getRightPadding());
		printFrame.setHeight(fillSubreport.getContentsStretchHeight() + lineBox.getTopPadding() + lineBox.getBottomPadding());
		if (fillSubreport.getTableReport().getBaseReport().isGeneratePdfTags())
		{
			printFrame.getPropertiesMap().setProperty(JRPdfExporterTagHelper.PROPERTY_TAG_TABLE, JRPdfExporterTagHelper.TAG_FULL);
		}
		
		List<JRStyle> styles = fillSubreport.getSubreportStyles();
		for (Iterator<JRStyle> it = styles.iterator(); it.hasNext();)
		{
			JRStyle style = it.next();
			try
			{
				fillContext.getFiller().addPrintStyle(style);
			}
			catch (JRException e)
			{
				throw new JRRuntimeException(e);
			}
		}
		
		List<JROrigin> origins = fillSubreport.getSubreportOrigins();
		for (Iterator<JROrigin> it = origins.iterator(); it.hasNext();)
		{
			JROrigin origin = it.next();
			fillContext.getFiller().getJasperPrint().addOrigin(origin);
		}
		
		Collection<JRPrintElement> elements = fillSubreport.getPrintElements();
		if (elements != null)
		{
			for (Iterator<JRPrintElement> it = elements.iterator(); it.hasNext();)
			{
				JRPrintElement element = it.next();
				printFrame.addElement(element);
			}
		}
		
		fillSubreport.subreportPageFilled();
		
		return printFrame;
	}

	protected JRTemplateFrame getFrameTemplate()
	{
		JRStyle style = fillContext.getElementStyle();
		JRTemplateFrame frameTemplate = printFrameTemplates.get(style);
		if (frameTemplate == null)
		{
			frameTemplate = new JRTemplateFrame(
						fillContext.getElementOrigin(),
						fillContext.getDefaultStyleProvider());
			frameTemplate.setElement(fillContext.getComponentElement());
			frameTemplate = deduplicate(frameTemplate);
			
			printFrameTemplates.put(style, frameTemplate);
		}

		return frameTemplate;
	}

	@Override
	public void rewind()
	{
		if (filling)
		{
			if (log.isDebugEnabled())
			{
				log.debug("Rewinding table subreport");
			}
			
			try
			{
				fillSubreport.rewind();
			}
			catch (JRException e)
			{
				throw new JRRuntimeException(e);
			}
			
			filling = false;
		}
	}

	protected class FillTableSubreportFactory
	{
		private final TableSubreport subreport;
		private final TableJasperReport compiledTableReport;
		private final BuiltinExpressionEvaluatorFactory builtinEvaluatorFactory;
		
		public FillTableSubreportFactory(TableSubreport subreport, TableJasperReport compiledTableReport, 
				BuiltinExpressionEvaluatorFactory builtinEvaluatorFactory)
		{
			this.subreport = subreport;
			this.compiledTableReport = compiledTableReport;
			this.builtinEvaluatorFactory = builtinEvaluatorFactory;
		}

		public FillTableSubreport createFillSubreport()
		{
			return new FillTableSubreport(
						fillContext, subreport, factory, compiledTableReport,
						builtinEvaluatorFactory
						);
		}
	}
	
}
