/*
 * JasperReports - Free Java Reporting Library.
 * Copyright (C) 2001 - 2023 Cloud Software Group, 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.fill;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

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

import net.sf.jasperreports.engine.BookmarkHelper;
import net.sf.jasperreports.engine.BookmarkIterator;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRGroup;
import net.sf.jasperreports.engine.JROrigin;
import net.sf.jasperreports.engine.JRPrintPage;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRScriptletException;
import net.sf.jasperreports.engine.JRStyle;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.PrintPart;
import net.sf.jasperreports.engine.part.DelayedPrintPart;
import net.sf.jasperreports.engine.part.FillPart;
import net.sf.jasperreports.engine.part.FillPartPrintOutput;
import net.sf.jasperreports.engine.part.FillParts;
import net.sf.jasperreports.engine.part.FillPrintPart;
import net.sf.jasperreports.engine.part.FillPrintPartQueue;
import net.sf.jasperreports.engine.part.FillingPrintPart;
import net.sf.jasperreports.engine.part.FinalFillingPrintPart;
import net.sf.jasperreports.engine.part.GroupFillParts;
import net.sf.jasperreports.engine.part.PartEvaluationTime;
import net.sf.jasperreports.engine.part.PartPrintOutput;
import net.sf.jasperreports.engine.type.IncrementTypeEnum;
import net.sf.jasperreports.engine.type.ResetTypeEnum;
import net.sf.jasperreports.engine.type.SectionTypeEnum;
import net.sf.jasperreports.engine.util.JRDataUtils;
import net.sf.jasperreports.parts.PartFillerParent;

/**
 * @author Lucian Chirita (lucianc@users.sourceforge.net)
 */
public class PartReportFiller extends BaseReportFiller
{
	private static final Log log = LogFactory.getLog(PartReportFiller.class);
	
	public static final String EXCEPTION_MESSAGE_KEY_EVALUATION_GROUP_NOT_FOUND = "fill.part.filler.evaluation.group.not.found";
	public static final String EXCEPTION_MESSAGE_KEY_UNKNOWN_EVALUATION_TIME_TYPE = "fill.part.filler.unknown.evaluation.time.type";
	public static final String EXCEPTION_MESSAGE_KEY_UNSUPPORTED_SECTION_TYPE = "fill.part.filler.unsupported.section.type";
	
	private FillParts detailParts;
	private List<GroupFillParts> groupParts;
	private Map<String, GroupFillParts> groupPartsByName;
	
	private FillPrintPartQueue partQueue;
	
	private List<DelayedPrintPart> reportEvaluatedParts;
	
	public PartReportFiller(JasperReportsContext jasperReportsContext, JasperReport jasperReport) throws JRException
	{
		this(jasperReportsContext, SimpleJasperReportSource.from(jasperReport), null);
	}
	
	public PartReportFiller(JasperReportsContext jasperReportsContext, JasperReport jasperReport, 
			PartFillerParent parent) throws JRException
	{
		this(jasperReportsContext, SimpleJasperReportSource.from(jasperReport), parent);
	}
	
	public PartReportFiller(JasperReportsContext jasperReportsContext, JasperReportSource reportSource, 
			PartFillerParent parent) throws JRException
	{
		super(jasperReportsContext, reportSource, parent);
		
		detailParts = new FillParts(jasperReport.getDetailSection(), factory);
		
		JRGroup[] reportGroups = jasperReport.getGroups();
		if (reportGroups == null || reportGroups.length == 0)
		{
			groupParts = Collections.emptyList();
			groupPartsByName = Collections.emptyMap();
		}
		else
		{
			groupParts = new ArrayList<>(reportGroups.length);
			groupPartsByName = new HashMap<>();
			for (JRGroup reportGroup : reportGroups)
			{
				GroupFillParts groupFillParts = new GroupFillParts(reportGroup, factory);
				groupParts.add(groupFillParts);
				groupPartsByName.put(reportGroup.getName(), groupFillParts);
			}
		}
		
		initDatasets();
		
		reportEvaluatedParts = new ArrayList<>();
		
		if (parent == null)
		{
			JasperPrintPartOutput jasperPrintOutput = new JasperPrintPartOutput();
			partQueue = new FillPrintPartQueue(jasperPrintOutput);
		}
		else
		{
			partQueue = parent.getFiller().partQueue;
		}
	}

	@Override
	protected void jasperReportSet()
	{
		if (jasperReport.getSectionType() != SectionTypeEnum.PART)
		{
			throw 
				new JRRuntimeException(
					EXCEPTION_MESSAGE_KEY_UNSUPPORTED_SECTION_TYPE,
					new Object[]{jasperReport.getSectionType()});
		}
	}

	@Override
	protected JRFillObjectFactory initFillFactory()
	{
		return new JRFillObjectFactory(this);
	}

	@Override
	public JasperPrint fill(Map<String, Object> parameterValues) throws JRException
	{
		//FIXMEBOOK copied from JRBaseFiller
		if (parameterValues == null)
		{
			parameterValues = new HashMap<>();
		}

		if (log.isDebugEnabled())
		{
			log.debug("Fill " + fillerId + ": filling report");
		}

		setParametersToContext(parameterValues);

		fillingThread = Thread.currentThread();
		
		JRResourcesFillUtil.ResourcesFillContext resourcesContext = 
			JRResourcesFillUtil.setResourcesFillContext(parameterValues);
		
		boolean success = false;
		try
		{
			createBoundElemementMaps();

/*			if (parent != null)
			{
				parent.registerSubfiller(this);
			}
*/
			setParameters(parameterValues);

			setBookmarkHelper();
			
			//loadStyles();

			jasperPrint.setName(jasperReport.getName());
			jasperPrint.setPageWidth(jasperReport.getPageWidth());
			jasperPrint.setPageHeight(jasperReport.getPageHeight());
			jasperPrint.setTopMargin(jasperReport.getTopMargin());
			jasperPrint.setLeftMargin(jasperReport.getLeftMargin());
			jasperPrint.setBottomMargin(jasperReport.getBottomMargin());
			jasperPrint.setRightMargin(jasperReport.getRightMargin());
			jasperPrint.setOrientation(jasperReport.getOrientationValue());

			jasperPrint.setFormatFactoryClass(jasperReport.getFormatFactoryClass());
			jasperPrint.setLocaleCode(JRDataUtils.getLocaleCode(getLocale()));
			jasperPrint.setTimeZoneId(JRDataUtils.getTimeZoneId(getTimeZone()));

			propertiesUtil.transferProperties(mainDataset, jasperPrint, 
				JasperPrint.PROPERTIES_PRINT_TRANSFER_PREFIX);

/*			jasperPrint.setDefaultStyle(defaultStyle);

			if (styles != null && styles.length > 0)
			{
				for (int i = 0; i < styles.length; i++)
				{
					addPrintStyle(styles[i]);
				}
			}
*/
			/*   */
			mainDataset.start();

			/*   */
			fillReport();
			
			if (bookmarkHelper != null)
			{
				jasperPrint.setBookmarks(bookmarkHelper.getRootBookmarks());
			}

			if (log.isDebugEnabled())
			{
				log.debug("Fill " + fillerId + ": ended");
			}

			success = true;
			return jasperPrint;
		}
		finally
		{
			mainDataset.closeDatasource();
			mainDataset.disposeParameterContributors();
			
			if (success && parent == null)
			{
				// commit the cached data
				fillContext.cacheDone();
			}

/*			if (parent != null)
			{
				parent.unregisterSubfiller(this);
			}
*/			
			delayedActions.dispose();

			clearFillingThread();

			//kill the subreport filler threads
			//killSubfillerThreads();
			
			if (parent == null)
			{
				fillContext.dispose();
			}

			JRResourcesFillUtil.revertResourcesFillContext(resourcesContext);
		}
	}

	private void createBoundElemementMaps()
	{
		createBoundElementMaps(JREvaluationTime.EVALUATION_TIME_MASTER);
	}

	@Override
	protected void ignorePaginationSet(Map<String, Object> parameterValues)
	{
		//NOP
	}
	
	protected void fillReport() throws JRException
	{
		if (mainDataset.next())
		{
			startReport();
			
			fillFirstGroupHeaders();
			calculateDetail();
			fillDetail();

			while (mainDataset.next())
			{
				checkInterrupted();
				estimateGroups();
				fillChangedGroupFooters();
				fillChangedGroupEvaluatedParts();
				
				calculateGroups();
				fillChangedGroupHeaders();
				
				calculateDetail();
				fillDetail();
			}
			
			fillLastGroupFooters();
			fillLastGroupEvaluatedParts();
		}
		else
		{
			//no records
			//might not be required, but it's safer to do it because it used to happen before mainDataset.next() 
			startReport();
		}
		
		fillReportEvaluatedParts();
		assert partQueue.isCollapsed();
		
		if (isMasterReport())
		{
			resolveMasterBoundElements();
		}
	}

	protected void startReport() throws JRScriptletException, JRException
	{
		scriptlet.callBeforeReportInit();
		calculator.initializeVariables(ResetTypeEnum.REPORT, IncrementTypeEnum.REPORT);
		scriptlet.callAfterReportInit();
	}

	protected void calculateDetail() throws JRScriptletException, JRException
	{
		scriptlet.callBeforeDetailEval();
		calculator.calculateVariables(true);
		scriptlet.callAfterDetailEval();
	}

	protected void estimateGroups() throws JRException
	{
		calculator.estimateGroupRuptures();
	}
	
	protected void calculateGroups() throws JRException
	{
		scriptlet.callBeforeGroupInit();
		calculator.initializeVariables(ResetTypeEnum.GROUP, IncrementTypeEnum.GROUP);
		scriptlet.callAfterGroupInit();
	}

	protected void fillDetail() throws JRException
	{
		fillParts(detailParts, JRExpression.EVALUATION_DEFAULT);
	}

	protected void fillFirstGroupHeaders() throws JRException
	{
		for (GroupFillParts group : groupParts)
		{
			fillParts(group.getHeaderParts(), JRExpression.EVALUATION_DEFAULT);
		}
	}

	protected void fillChangedGroupHeaders() throws JRException
	{
		for (GroupFillParts group : groupParts)
		{
			if (group.hasChanged())
			{
				fillParts(group.getHeaderParts(), JRExpression.EVALUATION_DEFAULT);
			}
		}
	}

	private void fillChangedGroupFooters() throws JRException
	{
		for (ListIterator<GroupFillParts> iterator = groupParts.listIterator(groupParts.size()); iterator.hasPrevious();)
		{
			GroupFillParts group = iterator.previous();
			if (group.hasChanged())
			{
				fillParts(group.getFooterParts(), JRExpression.EVALUATION_OLD);
			}
		}
	}

	private void fillLastGroupFooters() throws JRException
	{
		for (ListIterator<GroupFillParts> iterator = groupParts.listIterator(groupParts.size()); iterator.hasPrevious();)
		{
			GroupFillParts group = iterator.previous();
			fillParts(group.getFooterParts(), JRExpression.EVALUATION_DEFAULT);
		}
	}

	protected void fillParts(FillParts parts, byte evaluation) throws JRException
	{
		for (FillPart part : parts.getParts())
		{
			checkInterrupted();
			fillPart(part, evaluation);
		}
	}

	protected void fillPart(FillPart part, byte evaluation) throws JRException
	{
		PartEvaluationTime evaluationTime = part.getEvaluationTime();
		switch (evaluationTime.getEvaluationTimeType())
		{
		case NOW:
		{
			PartPrintOutput appendOutput = partQueue.tail().getOutput();
			if (appendOutput != null)
			{
				// can write directly to the previous output
				part.fill(evaluation, appendOutput);
			}
			else
			{
				// previous part is delayed, creating a new part with local output
				FillPartPrintOutput localOutput = new FillPartPrintOutput(this);
				part.fill(evaluation, localOutput);
				
				// adding to the queue
				partQueue.appendOutput(localOutput);
			}
			break;
		}
		case REPORT:
		{
			DelayedPrintPart delayedPart = partQueue.appendDelayed(part);
			reportEvaluatedParts.add(delayedPart);
			break;
		}
		case GROUP:
		{
			GroupFillParts groupFillParts = groupPartsByName.get(evaluationTime.getEvaluationGroup());
			if (groupFillParts == null)//verified at compile time, checking twice nonetheless
			{
				throw 
					new JRRuntimeException(
						EXCEPTION_MESSAGE_KEY_EVALUATION_GROUP_NOT_FOUND,
						new Object[]{evaluationTime.getEvaluationGroup()});
			}
			
			DelayedPrintPart delayedPart = partQueue.appendDelayed(part);
			groupFillParts.addGroupEvaluatedPart(delayedPart);
			break;
		}
		default:
			throw 
				new JRRuntimeException(
					EXCEPTION_MESSAGE_KEY_UNKNOWN_EVALUATION_TIME_TYPE,
					new Object[]{evaluationTime.getEvaluationTimeType()});
		}
	}

	@Override
	public boolean isPageFinal(int pageIndex)
	{
		if (isSubreport())
		{
			// shouldn't be called
			return false;
		}
		
		return ((JasperPrintPartOutput) partQueue.head().getOutput()).isPageFinal(pageIndex);
	}

	protected void partPageUpdated(int pageIndex)
	{
		if (fillListener != null)
		{
			fillListener.pageUpdated(jasperPrint, pageIndex);
		}
	}

	protected void fillReportEvaluatedParts() throws JRException
	{
		fillDelayedEvaluatedParts(reportEvaluatedParts, JRExpression.EVALUATION_DEFAULT);
	}

	protected void fillChangedGroupEvaluatedParts() throws JRException
	{
		for (GroupFillParts group : groupParts)
		{
			if (group.hasChanged())
			{
				fillDelayedEvaluatedParts(group.getGroupEvaluatedParts(), JRExpression.EVALUATION_OLD);
			}
		}
	}

	protected void fillLastGroupEvaluatedParts() throws JRException
	{
		for (GroupFillParts group : groupParts)
		{
			fillDelayedEvaluatedParts(group.getGroupEvaluatedParts(), JRExpression.EVALUATION_DEFAULT);
		}
	}
	
	protected void fillDelayedEvaluatedParts(List<DelayedPrintPart> parts, byte evaluation) throws JRException
	{
		for (ListIterator<DelayedPrintPart> it = parts.listIterator(); it.hasNext();)
		{
			DelayedPrintPart part = it.next();
			it.remove();
			
			fillDelayedPart(evaluation, part);
		}
	}

	protected void fillDelayedPart(byte evaluation, DelayedPrintPart part) throws JRException
	{
		partQueue.fillDelayed(part, this, evaluation);
	}

	public BookmarkHelper getFirstBookmarkHelper()
	{
		for(FillPrintPart part = partQueue.head(); part != null; part = part.nextPart())
		{
			PartPrintOutput output = part.getOutput();
			if (output != null)
			{
				BookmarkHelper bookmarks = output.getBookmarkHelper();
				if (bookmarks.hasBookmarks())
				{
					return bookmarks;
				}
			}
		}
		return null;
	}
	
	protected class JasperPrintPartOutput implements PartPrintOutput
	{
		private final ReadWriteLock currentFillPartLock = new ReentrantReadWriteLock();
		private transient int currentPartStartIndex;
		private FillingPrintPart currentFillingPart;
		
		@Override
		public void startPart(PrintPart part, FillingPrintPart fillingPart)
		{
			int startIndex = jasperPrint.getPages().size();
			jasperPrint.addPart(startIndex, part);

			currentFillPartLock.writeLock().lock();
			try
			{
				currentPartStartIndex = startIndex;
				currentFillingPart = fillingPart;
			}
			finally
			{
				currentFillPartLock.writeLock().unlock();
			}
			
			if (log.isDebugEnabled())
			{
				log.debug("added part " + part.getName() + " at index " + startIndex);
			}
		}
		
		public boolean isPageFinal(int pageIndex)
		{
			currentFillPartLock.readLock().lock();
			try
			{
				JRPrintPage page = getPage(pageIndex);
				boolean hasMasterActions = delayedActions.hasDelayedActions(page);
				if (hasMasterActions)
				{
					return false;
				}
				
				boolean isFinal;
				if (pageIndex < currentPartStartIndex)
				{
					isFinal = true;
				}
				else
				{
					isFinal = currentFillingPart.isPageFinal(page);
				}
				return isFinal;
			}
			finally
			{
				currentFillPartLock.readLock().unlock();
			}
		}

		@Override
		public void addPage(JRPrintPage page, DelayedFillActions delayedActionsSource)
		{
			int pageIndex = jasperPrint.getPages().size();
			if (log.isDebugEnabled())
			{
				log.debug("adding part page at index " + pageIndex);
			}
			
			jasperPrint.addPage(page);
			addLastPageBookmarks();
			
			delayedActions.moveMasterEvaluations(delayedActionsSource, page, pageIndex);
			
			if (fillListener != null)
			{
				fillListener.pageGenerated(jasperPrint, pageIndex);
			}
		}
		
		@Override
		public JRPrintPage getPage(int pageIndex)
		{
			return jasperPrint.getPages().get(pageIndex);
		}

		@Override
		public void pageUpdated(int partPageIndex)
		{
			partPageUpdated(currentPartStartIndex + partPageIndex);
		}

		@Override
		public void append(FillPartPrintOutput output)
		{
			addStyles(output.getStyles());
			addOrigins(output.getOrigins());
			
			int pageOffset = jasperPrint.getPages().size();
			BookmarkHelper outputBookmarks = output.getBookmarkHelper();
			BookmarkIterator bookmarkIterator = null;
			if (bookmarkHelper != null && outputBookmarks != null)
			{
				bookmarkIterator = outputBookmarks.bookmarkIterator();
			}
			
			DelayedFillActions sourceActions = output.getDelayedActions();
			
			// adding in an organic order: for each part the pages that belong to the part,
			// and for each page the bookmarks that belong to the page
			ListIterator<JRPrintPage> pagesIterator = output.getPages().listIterator();
			int prevPartStart = 0;
			for (Map.Entry<Integer, PrintPart> partEntry : output.getParts().entrySet())
			{
				int partStart = partEntry.getKey();
				// add the pages that belong to the previous part
				for (int i = prevPartStart; i < partStart; i++)
				{
					JRPrintPage page = pagesIterator.next();
					addPage(page, pageOffset, sourceActions, bookmarkIterator);
				}
				prevPartStart = partStart;
				
				PrintPart part = partEntry.getValue();
				startPart(part, FinalFillingPrintPart.instance());
			}

			// add the pages that belong to the last part
			while (pagesIterator.hasNext())
			{
				JRPrintPage page = pagesIterator.next();
				addPage(page, pageOffset, sourceActions, bookmarkIterator);
			}
		}
		
		protected void addPage(JRPrintPage page, int pageOffset, 
				DelayedFillActions sourceActions, BookmarkIterator sourceBookmarkIterator)
		{
			int pageIndex = jasperPrint.getPages().size();
			if (log.isDebugEnabled())
			{
				log.debug("adding part page at index " + pageIndex);
			}
			
			jasperPrint.addPage(page);
			
			if (sourceBookmarkIterator != null)
			{
				int sourcePageIndex = pageIndex - pageOffset;
				while (sourceBookmarkIterator.hasBookmark() 
						&& sourceBookmarkIterator.bookmark().getPageIndex() == sourcePageIndex)
				{
					bookmarkHelper.addBookmark(sourceBookmarkIterator.bookmark(), pageOffset);
					sourceBookmarkIterator.next();
				}
			}
			
			delayedActions.moveMasterEvaluations(sourceActions, page, pageIndex);
			
			if (fillListener != null)
			{
				fillListener.pageGenerated(jasperPrint, pageIndex);
			}
		}

		@Override
		public BookmarkHelper getBookmarkHelper()
		{
			return bookmarkHelper;
		}

		@Override
		public void addStyles(Collection<JRStyle> stylesList)
		{
			for (JRStyle style : stylesList)
			{
				try
				{
					jasperPrint.addStyle(style, true);
				}
				catch (JRException e)
				{
					// should not happen
					throw new JRRuntimeException(e);
				}
			}
		}

		@Override
		public void addOrigins(Collection<JROrigin> origins)
		{
			for (JROrigin origin : origins)
			{
				jasperPrint.addOrigin(origin);
			}
		}
	}

}
