/*
 * 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.components.headertoolbar.json;

import java.awt.GraphicsEnvironment;
import java.nio.CharBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;

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

import com.fasterxml.jackson.core.io.JsonStringEncoder;

import net.sf.jasperreports.components.headertoolbar.HeaderToolbarElement;
import net.sf.jasperreports.components.headertoolbar.HeaderToolbarElementUtils;
import net.sf.jasperreports.components.headertoolbar.actions.ConditionalFormattingData;
import net.sf.jasperreports.components.headertoolbar.actions.EditTextElementData;
import net.sf.jasperreports.components.headertoolbar.actions.FilterAction;
import net.sf.jasperreports.components.headertoolbar.actions.SortAction;
import net.sf.jasperreports.components.sort.FieldFilter;
import net.sf.jasperreports.components.sort.FilterTypeBooleanOperatorsEnum;
import net.sf.jasperreports.components.sort.FilterTypeDateOperatorsEnum;
import net.sf.jasperreports.components.sort.FilterTypeNumericOperatorsEnum;
import net.sf.jasperreports.components.sort.FilterTypeTextOperatorsEnum;
import net.sf.jasperreports.components.sort.FilterTypesEnum;
import net.sf.jasperreports.components.sort.actions.FilterCommand;
import net.sf.jasperreports.components.sort.actions.FilterData;
import net.sf.jasperreports.components.sort.actions.SortData;
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.GroupCell;
import net.sf.jasperreports.components.table.StandardColumn;
import net.sf.jasperreports.components.table.StandardTable;
import net.sf.jasperreports.components.table.fill.TableReport;
import net.sf.jasperreports.components.table.util.TableUtil;
import net.sf.jasperreports.engine.DatasetFilter;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRExpressionChunk;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRGenericPrintElement;
import net.sf.jasperreports.engine.JRIdentifiable;
import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.JRPropertiesUtil;
import net.sf.jasperreports.engine.JRSortField;
import net.sf.jasperreports.engine.JRStyle;
import net.sf.jasperreports.engine.JRStyleContainer;
import net.sf.jasperreports.engine.JRVariable;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.ReportContext;
import net.sf.jasperreports.engine.design.JRDesignComponentElement;
import net.sf.jasperreports.engine.design.JRDesignDataset;
import net.sf.jasperreports.engine.design.JRDesignDatasetRun;
import net.sf.jasperreports.engine.design.JRDesignTextElement;
import net.sf.jasperreports.engine.design.JRDesignTextField;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.export.GenericElementJsonHandler;
import net.sf.jasperreports.engine.export.JsonExporterContext;
import net.sf.jasperreports.engine.fonts.FontUtil;
import net.sf.jasperreports.engine.type.ModeEnum;
import net.sf.jasperreports.engine.type.NamedEnum;
import net.sf.jasperreports.engine.type.SortFieldTypeEnum;
import net.sf.jasperreports.engine.util.JRColorUtil;
import net.sf.jasperreports.engine.util.JRDataUtils;
import net.sf.jasperreports.engine.util.JRStringUtil;
import net.sf.jasperreports.repo.JasperDesignCache;
import net.sf.jasperreports.util.JacksonUtil;
import net.sf.jasperreports.web.commands.CommandTarget;
import net.sf.jasperreports.web.util.VelocityUtil;


/**
 * @author Teodor Danciu (teodord@users.sourceforge.net)
 */
public class HeaderToolbarElementJsonHandler implements GenericElementJsonHandler
{
	private static final Log log = LogFactory.getLog(HeaderToolbarElementJsonHandler.class);
	
	private static final String HEADER_TOOLBAR_ELEMENT_JSON_TEMPLATE = "net/sf/jasperreports/components/headertoolbar/json/resources/HeaderToolbarElementJsonTemplate.vm";
	private static final String PARAM_GENERATED_TEMPLATE_PREFIX = "net.sf.jasperreports.headertoolbar.";

	private static final List<String> datePatterns = new ArrayList<>(); 
	private static final List<String> timePatterns = new ArrayList<>(); 
	private static final Map<String, String> numberPatternsMap = new LinkedHashMap<>();

	private static final String DURATION_PATTERN = "[h]:mm:ss";
	
	static {
		// date patterns
		datePatterns.add("dd/MM/yyyy");
		datePatterns.add("MM/dd/yyyy");
		datePatterns.add("yyyy/MM/dd");
		datePatterns.add("EEEEE dd MMMMM yyyy");
		datePatterns.add("MMMMM dd. yyyy");
		datePatterns.add("dd/MM");
		datePatterns.add("dd/MM/yy");
		datePatterns.add("dd-MMM");
		datePatterns.add("dd-MMM-yy");
		datePatterns.add("MMM-yy");
		datePatterns.add("MMMMM-yy");
		datePatterns.add("dd/MM/yyyy h.mm a");
		datePatterns.add("dd/MM/yyyy HH.mm.ss");
		datePatterns.add("MMM");
		datePatterns.add("d/M/yyyy");
		datePatterns.add("dd-MMM-yyyy");
		datePatterns.add("yyyy.MM.dd G 'at' HH:mm:ss z");
		datePatterns.add("EEE. MMM d. ''yy");
		datePatterns.add("yyyy.MMMMM.dd GGG hh:mm aaa");
		datePatterns.add("EEE. d MMM yyyy HH:mm:ss Z");
		datePatterns.add("yyMMddHHmmssZ");
		
		timePatterns.add("hh:mm aaa");
		timePatterns.add("hh:mm:ss aaa");
		timePatterns.add("HH:mm");
		timePatterns.add("HH:mm:ss");

		numberPatternsMap.put("###0;-###0", "-1234");
		numberPatternsMap.put("###0;###0-", "1234-");
		numberPatternsMap.put("###0;(###0)", "(1234)");
		numberPatternsMap.put("###0;(-###0)", "(-1234)");
		numberPatternsMap.put("###0;(###0-)", "(1234-)");
		numberPatternsMap.put(DURATION_PATTERN, "[h]:mm:ss");
	}
	
	private static final String TABLE_UUID = "exporter_first_attempt";

	@Override
	public String getJsonFragment(JsonExporterContext context, JRGenericPrintElement element)
	{
		boolean templateAlreadyLoaded = false;

		String htmlFragment = null;
		String tableUUID = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_TABLE_UUID);
		ReportContext reportContext = context.getExporterRef().getReportContext();
		//checking if interactivity is disabled at export time
		boolean interactive = JRPropertiesUtil.getInstance(context.getJasperReportsContext()).getBooleanProperty(
				TableReport.PROPERTY_INTERACTIVE_TABLE, true);
		if (reportContext != null && tableUUID != null && interactive)//FIXMEJIVE
		{
			String columnUuid = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_UUID);
			String columnLabel = (String)element.getParameterValue(HeaderToolbarElement.PARAMETER_COLUMN_LABEL);
			if (columnLabel == null) {
				columnLabel = "";
			}
			int columnIndex = Integer.parseInt(element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_INDEX));
			
			Map<String, Object> contextMap = new HashMap<>();
			contextMap.put("JRStringUtil", JRStringUtil.class);
			contextMap.put("tableUUID", tableUUID);
			
			JasperReportsContext jrContext = context.getJasperReportsContext();
			
			JasperPrint jasperPrint = context.getExportedReport();
			Locale locale = jasperPrint.getLocaleCode() == null ? Locale.getDefault() : JRDataUtils.getLocale(jasperPrint.getLocaleCode());
			TimeZone timeZone = getFilterTimeZone(jasperPrint, element);
			
			if (log.isDebugEnabled())
			{
				log.debug("column " + columnUuid + " has locale " + locale + ", timezone " + timeZone);
			}
			
			Boolean isClearCache = (Boolean)reportContext.getParameterValue(PARAMETER_CLEAR_CONTEXT_CACHE);

			if (reportContext.getParameterValue(PARAM_GENERATED_TEMPLATE_PREFIX) != null && !(isClearCache != null && isClearCache)) {
				templateAlreadyLoaded = true;
			} else {
				reportContext.setParameterValue(PARAM_GENERATED_TEMPLATE_PREFIX, true);
			}
			
			ColumnFormatting.store(reportContext, tableUUID, columnIndex, locale, timeZone);
			
			/*** begin: FILTER PATTERNS ***/
			// numeric filter pattern
			String numericFilterPattern = HeaderToolbarElementUtils.getNumberPattern(jrContext, locale);

			// date filter pattern
			String dateFilterPattern = HeaderToolbarElementUtils.getDatePattern(jrContext, locale);

			// time filter pattern
			String timeFilterPattern = HeaderToolbarElementUtils.getTimePattern(jrContext, locale);
			/*** end: FILTER PATTERNS ***/

			FilterAction action = new FilterAction();
			action.init(jrContext, reportContext);
			CommandTarget target = action.getCommandTarget(UUID.fromString(tableUUID), false);

			JasperDesign jasperDesign = null;
			JRDesignDataset dataset = null;
			StandardTable table = null;

			if (target != null) 
			{
				JRIdentifiable identifiable = target.getIdentifiable();
				JRDesignComponentElement componentElement = identifiable instanceof JRDesignComponentElement ? (JRDesignComponentElement)identifiable : null;
				table = componentElement == null ? null : (StandardTable)componentElement.getComponent();
				JasperDesignCache cache = JasperDesignCache.getInstance(jrContext, reportContext);
				jasperDesign = cache.getJasperDesign(target.getUri(), false);
				JRDesignDatasetRun datasetRun = (JRDesignDatasetRun)table.getDatasetRun();
				String datasetName = datasetRun.getDatasetName();
				dataset = (JRDesignDataset)jasperDesign.getDatasetMap().get(datasetName);
			}
			
			if (!tableUUID.equals(context.getValue(TABLE_UUID))) 
			{
				Map<String, ColumnInfo> columnNames = getAllColumnNames(element, jrContext, contextMap);
				List<Map<String, Object>> columnGroupsData = getColumnGroupsData(jrContext, reportContext, target, jasperDesign, dataset, table, tableUUID, locale, timeZone);
				// column names are normally set on the first column, but check if we got them
				if (!columnNames.isEmpty()) {
					context.setValue(TABLE_UUID, tableUUID);

					// column info
					contextMap.put("allColumnNames", JacksonUtil.getInstance(jrContext).getJsonString(columnNames));
					contextMap.put("allColumnGroupsData", JacksonUtil.getInstance(jrContext).getJsonString(columnGroupsData));
					contextMap.put("tableName", element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_TABLE_NAME));

					// floating header
					contextMap.put("hasFloatingHeader", Boolean.parseBoolean(element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_FLOATING_HEADER)));

					// patterns
					contextMap.put("numericPatterns", JacksonUtil.getInstance(jrContext).getJsonString(getNumberPatterns(numberPatternsMap)));
					contextMap.put("datePatterns", JacksonUtil.getInstance(jrContext).getJsonString(getDatePatterns(datePatterns, locale)));
					contextMap.put("timePatterns", JacksonUtil.getInstance(jrContext).getJsonString(getDatePatterns(timePatterns, locale)));

					// operators
					contextMap.put("numericOperators", JacksonUtil.getInstance(jrContext).getJsonString(getOperators(FilterTypeNumericOperatorsEnum.values())));
					contextMap.put("dateTimeOperators", JacksonUtil.getInstance(jrContext).getJsonString(getOperators(FilterTypeDateOperatorsEnum.values())));
					contextMap.put("textOperators", JacksonUtil.getInstance(jrContext).getJsonString(getOperators(FilterTypeTextOperatorsEnum.values())));
					contextMap.put("booleanOperators", JacksonUtil.getInstance(jrContext).getJsonString(getOperators(FilterTypeBooleanOperatorsEnum.values())));

					/*** begin: FILTER PATTERNS ***/
					// numeric filter pattern
					contextMap.put("numericFilterPattern", numericFilterPattern);

					// date filter pattern
					contextMap.put("dateFilterPattern", dateFilterPattern);

					// time filter pattern
					contextMap.put("timeFilterPattern", timeFilterPattern);
					/*** end: FILTER PATTERNS ***/

					/*** begin: CALENDAR PATTERNS ***/
					// date pattern
					contextMap.put("calendarDatePattern", HeaderToolbarElementUtils.getCalendarDatePattern(jrContext, locale));

					// time pattern
					contextMap.put("calendarTimePattern", HeaderToolbarElementUtils.getCalendarTimePattern(jrContext, locale));
					/*** end: CALENDAR PATTERNS ***/

					contextMap.put("exporterFirstAttempt", true);
				}
			}

			contextMap.put("templateAlreadyLoaded", templateAlreadyLoaded);

			Boolean canSort = Boolean.parseBoolean(element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_CAN_SORT));
			Boolean canFilter = Boolean.parseBoolean(element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_CAN_FILTER));

			if (element.getModeValue() == ModeEnum.OPAQUE)
			{
				contextMap.put("backgroundColor", JRColorUtil.getColorHexa(element.getBackcolor()));
			}

			contextMap.put("columnUuid", columnUuid);
			//FIXME conceptually it would be better not to encode for html here 
			//but produce a pure json and encode for html on the client where necessary
			contextMap.put("columnLabel", escapeJSONString(JRStringUtil.htmlEncode(columnLabel)));
			contextMap.put("columnIndex", columnIndex);
			contextMap.put("dataType", FilterTypesEnum.TEXT.getName()); // use Text as default
			contextMap.put("canSort", canSort);
			contextMap.put("canFilter", canFilter);

			contextMap.put("fontExtensionsFontNames", getFontExtensionsFontNames(jrContext));
			contextMap.put("systemFontNames", getSystemFontNames(jrContext));

			setColumnHeaderData(columnLabel, columnIndex, target, contextMap, jrContext, reportContext, locale);
			EditTextElementData columnValueData = setColumnValueData(columnIndex, target, contextMap, jrContext, reportContext, locale);

			String columnName = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_NAME);
			String columnComponentName = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_COMPONENT_NAME);
			String columnType = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_TYPE);
			FilterTypesEnum filterType = FilterTypesEnum.getByName(element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_FILTER_TYPE));

			if (columnComponentName == null) {
				columnComponentName = columnName;
			}
			contextMap.put("columnName", columnComponentName);

			if (canFilter)
			{
				FilterData filterData = getFilterData(jrContext, dataset, columnName, columnType, filterType, columnValueData.getFormatPattern(),
						locale, timeZone);

				contextMap.put("dataType", filterType.getName());
				contextMap.put("filterData", JacksonUtil.getInstance(jrContext).getJsonString(filterData));
				contextMap.put("filterTableUuid", filterData.getTableUuid());
			}
			
			if (canSort) {
				SortData sortAscData = new SortData(tableUUID, columnName, columnType, HeaderToolbarElement.SORT_ORDER_ASC);
				SortData sortDescData = new SortData(tableUUID, columnName, columnType, HeaderToolbarElement.SORT_ORDER_DESC);
				String sortField = getCurrentSortField(jrContext, reportContext, tableUUID, columnName, columnType);
				if (sortField != null) 
				{
					String[] sortActionData = HeaderToolbarElementUtils.extractColumnInfo(sortField);
					
					boolean isAscending = HeaderToolbarElement.SORT_ORDER_ASC.equals(sortActionData[2]);
					if (isAscending) {
						sortAscData.setSortOrder(HeaderToolbarElement.SORT_ORDER_NONE);
					} else {
						sortDescData.setSortOrder(HeaderToolbarElement.SORT_ORDER_NONE);
					}
				}
				contextMap.put("sortAscData", JacksonUtil.getInstance(jrContext).getJsonString(sortAscData));
				contextMap.put("sortDescData", JacksonUtil.getInstance(jrContext).getJsonString(sortDescData));
			}
			
			List<BaseColumn> tableColumns = TableUtil.getAllColumns(table);
			Column column = (Column)tableColumns.get(columnIndex);
			
			JRDesignTextField detailTextField = TableUtil.getCellElement(JRDesignTextField.class, column.getDetailCell(), true);
			if (detailTextField != null)
			{
				ConditionalFormattingData detailCfd = getConditionalFormattingData(element, jrContext, dataset, detailTextField, null,
						locale, timeZone);
				if (detailCfd != null)
				{
					contextMap.put("conditionalFormattingData", JacksonUtil.getInstance(jrContext).getJsonString(detailCfd));
				}
			}
			
			htmlFragment = VelocityUtil.processTemplate(HeaderToolbarElementJsonHandler.HEADER_TOOLBAR_ELEMENT_JSON_TEMPLATE, contextMap);
		}
		
		return htmlFragment;
	}

	protected TimeZone getFilterTimeZone(JasperPrint jasperPrint, JRGenericPrintElement element)
	{
		String formatTimeZone = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_FORMAT_TIME_ZONE);
		TimeZone timeZone;
		if (formatTimeZone != null)
		{
			timeZone = JRDataUtils.getTimeZone(formatTimeZone);
		}
		else if (jasperPrint.getTimeZoneId() != null)
		{
			timeZone = JRDataUtils.getTimeZone(jasperPrint.getTimeZoneId());
		}
		else
		{
			timeZone = TimeZone.getDefault();
		}
		return timeZone;
	}
	
	private List<HashMap<String, String>> getDatePatterns(List<String> datePatterns, Locale locale) {
		List<HashMap<String, String>> formatPatterns = new ArrayList<>();

		SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", locale);
		Date today = new Date();
		HashMap<String, String> keys;

		for(String datePattern: datePatterns) {
			keys = new HashMap<>();
			sdf.applyPattern(datePattern);
			keys.put("key", datePattern);
			keys.put("val", sdf.format(today));
			formatPatterns.add(keys);
		}

		return formatPatterns;
	}
	private List<HashMap<String, String>> getNumberPatterns(Map<String, String> numberPatternsMap) {
		List<HashMap<String, String>> formatPatterns = new ArrayList<>();
		HashMap<String, String> keys;

		for(Map.Entry<String, String> entry: numberPatternsMap.entrySet()) {
			keys = new HashMap<>();
			keys.put("key", entry.getKey());
			keys.put("val", entry.getValue());
			formatPatterns.add(keys);
		}

		return formatPatterns;
	}

	private String getCurrentSortField(
		JasperReportsContext jasperReportsContext,
		ReportContext reportContext, 
		String uuid, 
		String sortColumnName, 
		String sortColumnType
		) 
	{
		JasperDesignCache cache = JasperDesignCache.getInstance(jasperReportsContext, reportContext);
		SortAction action = new SortAction();
		action.init(jasperReportsContext, reportContext);
		CommandTarget target = action.getCommandTarget(UUID.fromString(uuid), false);
		if (target != null)
		{
			JRIdentifiable identifiable = target.getIdentifiable();
			JRDesignComponentElement componentElement = identifiable instanceof JRDesignComponentElement ? (JRDesignComponentElement)identifiable : null;
			StandardTable table = componentElement == null ? null : (StandardTable)componentElement.getComponent();
			
			JRDesignDatasetRun datasetRun = (JRDesignDatasetRun)table.getDatasetRun();
			
			String datasetName = datasetRun.getDatasetName();
			
			JasperDesign jasperDesign = cache.getJasperDesign(target.getUri(), false);//FIXMEJIVE getJasperReport not design
			JRDesignDataset dataset = (JRDesignDataset)jasperDesign.getDatasetMap().get(datasetName);
			
			List<JRSortField> existingFields =  dataset.getSortFieldsList();
			String sortField = null;
	
			if (existingFields != null && existingFields.size() > 0) {
				for (JRSortField field: existingFields) {
					if (field.getName().equals(sortColumnName) && field.getType().getName().equals(sortColumnType)) {
						sortField = sortColumnName + HeaderToolbarElement.SORT_COLUMN_TOKEN_SEPARATOR + sortColumnType + HeaderToolbarElement.SORT_COLUMN_TOKEN_SEPARATOR;
						switch (field.getOrderValue()) {
							case ASCENDING:
								sortField += HeaderToolbarElement.SORT_ORDER_ASC;
								break;
							case DESCENDING:
								sortField += HeaderToolbarElement.SORT_ORDER_DESC;
								break;
						}
						break;
					}
				}
			}
		
			return sortField;
		}
		
		return null;
	}
	
	@Override
	public boolean toExport(JRGenericPrintElement element) {
		return true;
	}
	
	private List<String> getOperators(NamedEnum[] operators)
	{
		List<String> result = new ArrayList<>();
		for (NamedEnum operator: operators)
		{
			result.add(((Enum<?>)operator).name());
		}
		return result;
	}
	
	private static FilterData getFilterData(
		JasperReportsContext jasperReportsContext,
		JRDesignDataset dataset,
		String columnName,
		String columnType,
		FilterTypesEnum filterType,
		String columnValuePattern,
		Locale locale,
		TimeZone timeZone
		) 
	{
		// get existing filter as JSON string
		String serializedFilters = "[]";
		JRPropertiesMap propertiesMap = dataset.getPropertiesMap();
		if (propertiesMap.getProperty(FilterCommand.DATASET_FILTER_PROPERTY) != null) {
			serializedFilters = propertiesMap.getProperty(FilterCommand.DATASET_FILTER_PROPERTY);
		}
		
		List<DatasetFilter> filters = new ArrayList<>();
		
		List<? extends DatasetFilter> existingFilters = JacksonUtil.getInstance(jasperReportsContext).loadList(serializedFilters, FieldFilter.class);
		if (existingFilters.size() > 0) {
			for (DatasetFilter filter: existingFilters) {
				if (((FieldFilter)filter).getField().equals(columnName)) {
					filters.add(filter);
					break;
				}
			}
		}
		
		FilterData filterData = new FilterData();
		filterData.setFieldName(columnName);
		filterData.setFilterType(filterType.getName());
		filterData.setIsField(SortFieldTypeEnum.FIELD.equals(SortFieldTypeEnum.getByName(columnType)));

		if (filters.size() > 0) 
		{
			FieldFilter ff = (FieldFilter)filters.get(0);
			filterData.setFieldValueStart(ff.getFilterValueStart());
			filterData.setFieldValueEnd(ff.getFilterValueEnd());
			filterData.setFilterTypeOperator(ff.getFilterTypeOperator());
			filterData.setFilterType(ff.getFilterType());
			filterData.setFilterPattern(ff.getFilterPattern());
			filterData.setLocaleCode(ff.getLocaleCode());
			filterData.setTimeZoneId(ff.getTimeZoneId());
		}

		if (filterType.getName().equals(filterData.getFilterType()))
		{
			String filterPattern = HeaderToolbarElementUtils.getFilterPattern(jasperReportsContext, locale, filterType);

			if (FilterTypesEnum.NUMERIC.equals(filterType) && DURATION_PATTERN.equals(columnValuePattern)) {
				filterPattern = DURATION_PATTERN;
			}

			HeaderToolbarElementUtils.updateFilterData(
				filterData,
				filterPattern,
				locale,
				timeZone
				);
		}
		else
		{
			//FIXMEJIVE should we raise error?
		}
		
		return filterData;
	}

	private void setColumnHeaderData(String sortColumnLabel, Integer columnIndex, CommandTarget target, Map<String, Object> contextMap,
			JasperReportsContext jasperReportsContext, ReportContext reportContext, Locale locale) {
		if (target != null){
			JRIdentifiable identifiable = target.getIdentifiable();
			JRDesignComponentElement componentElement = identifiable instanceof JRDesignComponentElement ? (JRDesignComponentElement)identifiable : null;
			StandardTable table = componentElement == null ? null : (StandardTable)componentElement.getComponent();

			List<BaseColumn> tableColumns = TableUtil.getAllColumns(table);

			if (columnIndex != null) {
				StandardColumn column = (StandardColumn) tableColumns.get(columnIndex);

				JRDesignTextElement textElement = TableUtil.getCellElement(JRDesignTextElement.class, column.getColumnHeader(), true);

				if (textElement != null) {
					EditTextElementData textElementData = new EditTextElementData();

					textElementData.setHeadingName(JRStringUtil.htmlEncode(sortColumnLabel));
					textElementData.setColumnIndex(columnIndex);
					JRDesignTextElement styledElement = resolveElementStyle(textElement, reportContext, target);
					HeaderToolbarElementUtils.copyTextElementStyle(textElementData, styledElement, locale);

					contextMap.put("colHeaderData", JacksonUtil.getInstance(jasperReportsContext).getJsonString(textElementData));
				}
			}
		}
	}
	
	private JRDesignTextElement resolveElementStyle(JRDesignTextElement element, ReportContext context, 
			CommandTarget target)
	{
		JRStyle defaultStyle = element.getDefaultStyleProvider() == null ? null 
				: element.getDefaultStyleProvider().getDefaultStyle();
		boolean hasExternalStyle = hasExternalStyle(element) 
				|| (defaultStyle != null && hasExternalStyle(defaultStyle));
		if (!hasExternalStyle && defaultStyle != null)
		{
			//we have everything
			return element;
		}
		
		JasperDesignCache designCache = JasperDesignCache.getExistingInstance(context);
		List<JRStyle> reportStyles = designCache == null ? null : designCache.getStyles(
				target.getUri(), target.getIdentifiable() == null ? null : target.getIdentifiable().getUUID());
		if (reportStyles == null || reportStyles.isEmpty())
		{
			return element;
		}
		
		JRStyle externalDefault = null;
		if (defaultStyle == null)
		{
			for (JRStyle style : reportStyles)
			{
				if (style.isDefault())
				{
					externalDefault = style;
				}
			}
			
			if (!hasExternalStyle && externalDefault == null)
			{
				//nothing to change
				return element;
			}
		}
		
		JRStyle elementStyle;
		if (element.getStyle() == null && element.getStyleNameReference() == null)
		{
			elementStyle = externalDefault;
		}
		else
		{
			String elementStyleName = element.getStyleNameReference() == null 
					? element.getStyle().getName() : element.getStyleNameReference();
					
			elementStyle = null;
			for (JRStyle style : reportStyles)
			{
				if (style.getName().equals(elementStyleName))
				{
					elementStyle = style;
				}
			}
		}
		
		if (elementStyle == null)
		{
			//didn't find a style for the element
			return element;
		}
		
		JRDesignTextElement elementClone = (JRDesignTextElement) element.clone();
		elementClone.setStyle(elementStyle);
		return elementClone;
	}
	
	private boolean hasExternalStyle(JRStyleContainer styleContainer)
	{
		if (styleContainer.getStyle() == null && styleContainer.getStyleNameReference() != null)
		{
			return true;
		}
		
		JRStyle style = styleContainer.getStyle();
		while (style != null)
		{
			if (style.getStyle() == null && style.getStyleNameReference() != null)
			{
				return true;
			}
			
			style = style.getStyle();
		}
		return false;
	}
	
	private EditTextElementData setColumnValueData(Integer columnIndex, CommandTarget target, Map<String, Object> contextMap,
			JasperReportsContext jasperReportsContext, ReportContext reportContext, Locale locale) {
		EditTextElementData textElementData = new EditTextElementData();

		if (target != null){
			JRIdentifiable identifiable = target.getIdentifiable();
			JRDesignComponentElement componentElement = identifiable instanceof JRDesignComponentElement ? (JRDesignComponentElement)identifiable : null;
			StandardTable table = componentElement == null ? null : (StandardTable)componentElement.getComponent();

			List<BaseColumn> tableColumns = TableUtil.getAllColumns(table);

			if (columnIndex != null) {
				StandardColumn column = (StandardColumn) tableColumns.get(columnIndex);

				JRDesignTextField textElement = TableUtil.getCellElement(JRDesignTextField.class, column.getDetailCell(), true);

				if (textElement != null) {
					textElementData.setColumnIndex(columnIndex);
					JRDesignTextElement styledElement = resolveElementStyle(textElement, reportContext, target);
					HeaderToolbarElementUtils.copyTextElementStyle(textElementData, styledElement, locale);
				}
			}
		}
		contextMap.put("colValueData", JacksonUtil.getInstance(jasperReportsContext).getJsonString(textElementData));

		return textElementData;
	}

	public static class ColumnInfo {
		private String index;
		private String label;
		private String uuid;
		private boolean visible;
		private boolean interactive;
		
		private ColumnInfo(String index, String label, String uuid, boolean visible, boolean interactive) {
			this.index = index;
			this.label = label;
			this.uuid = uuid;
			this.visible = visible;
			this.interactive = interactive;
		}
		
		public String getIndex() {
			return index;
		}
		
		public String getLabel() {
			return label;
		}

		public String getUuid() {
			return uuid;
		}
		
		public boolean getVisible() {
			return visible;
		}

		public boolean getInteractive() {
			return interactive;
		}
	}

	private Map<String, ColumnInfo> getAllColumnNames(JRGenericPrintElement element, 
			JasperReportsContext jasperReportsContext, Map<String, Object> contextMap) {
		int prefixLength = HeaderToolbarElement.PARAM_COLUMN_LABEL_PREFIX.length();
		Map<String, ColumnInfo> columnNames = new HashMap<>();
		for (String paramName : element.getParameterNames()) {
			if (paramName.startsWith(HeaderToolbarElement.PARAM_COLUMN_LABEL_PREFIX)) {
				String columnName = (String) element.getParameterValue(paramName);
				String[] tokens = paramName.substring(prefixLength).split("\\|");
				if (columnName == null || columnName.trim().length() == 0) {
					columnName = "Column_" + tokens[0];
				}
				columnNames.put(tokens[0], new ColumnInfo(tokens[0], JRStringUtil.htmlEncode(columnName), tokens[1], false, Boolean.valueOf(tokens[2])));
			}
		}
		return columnNames;
	}

	public static class GroupInfo {
		public static final String TYPE_GROUP_HEADING = "groupheading";
		public static final String TYPE_GROUP_SUBTOTAL = "groupsubtotal";
		public static final String TYPE_TABLE_TOTAL = "tabletotal";

		private String name;
		private String type;
		private List<Integer> forColumns;

		public GroupInfo(String name, String type) {
			this.name = name;
			this.type = type;
			this.forColumns = new ArrayList<>();
		}

		public String getName() {
			return name;
		}

		public String getType() {
			return type;
		}

		public void addForColumn(Integer index) {
			forColumns.add(index);
		}

		public List<Integer> getForColumns() {
			return this.forColumns;
		}
	}

	private boolean setTextElements(List<GroupCell> groupCells, Map<JRDesignTextElement, GroupInfo> groups, String groupType, Integer columnIndex) {
		boolean result = false;
		if (groupCells != null) {
			for (GroupCell gc: groupCells) {
				JRDesignTextElement textElement = TableUtil.getCellElement(JRDesignTextElement.class, gc.getCell(), false);

				if (textElement != null) {
					result = true;
					if (groups.containsKey(textElement)) {
						groups.get(textElement).addForColumn(columnIndex);
					}
					else {
						GroupInfo gi = new GroupInfo(gc.getGroupName() != null ? gc.getGroupName() : groupType + "_" + columnIndex, groupType);
						gi.addForColumn(columnIndex);
						groups.put(textElement, gi);
					}
				}
			}
		}
		return result;
	}

	private List<Map<String, Object>> getColumnGroupsData(JasperReportsContext jasperReportsContext, 
			ReportContext reportContext, CommandTarget target, JasperDesign jasperDesign,
			JRDesignDataset dataset, StandardTable table, String tableUuid, Locale locale, TimeZone timeZone)
	{
		List<BaseColumn> allColumns = TableUtil.getAllColumns(table);

		int i = 0;
		Map<JRDesignTextElement, GroupInfo> groups = new HashMap<>();
		boolean found;

		// build the groups map
		for (BaseColumn bc: allColumns) {
			List<ColumnGroup> colGroups = TableUtil.getHierarchicalColumnGroupsForColumn(bc, table.getColumns(), table);

			// group headers
			found = setTextElements(bc.getGroupHeaders(), groups, GroupInfo.TYPE_GROUP_HEADING, i);
			if (!found) {
				for (ColumnGroup cg: colGroups) {
					if (cg.getGroupHeaders() == null) {
						continue;
					}
					found = setTextElements(cg.getGroupHeaders(), groups, GroupInfo.TYPE_GROUP_HEADING, i);
					if (found) break;
				}
			}

			// group footers
			found = setTextElements(bc.getGroupFooters(), groups, GroupInfo.TYPE_GROUP_SUBTOTAL, i);
			if (!found) {
				for (ColumnGroup cg: colGroups) {
					if (cg.getGroupFooters() == null) {
						continue;
					}
					found = setTextElements(cg.getGroupFooters(), groups, GroupInfo.TYPE_GROUP_SUBTOTAL, i);
					if (found) break;
				}
			}

			// table footers
			found = false;
			if (bc.getTableFooter() != null) {
				JRDesignTextElement textElement = TableUtil.getCellElement(JRDesignTextElement.class, bc.getTableFooter(), false);

				if (textElement != null) {
					found = true;
					if (groups.containsKey(textElement)) {
						groups.get(textElement).addForColumn(i);
					}
					else {
						GroupInfo gi = new GroupInfo("Tabletotal" + "_" + i, GroupInfo.TYPE_TABLE_TOTAL);
						gi.addForColumn(i);
						groups.put(textElement, gi);
					}
				}
			}
			if (!found) {
				for (ColumnGroup cg: colGroups) {
					if (cg.getTableFooter() == null) {
						continue;
					}
					JRDesignTextElement textElement = TableUtil.getCellElement(JRDesignTextElement.class, cg.getTableFooter(), false);

					if (textElement != null) {
						found = true;
						if (groups.containsKey(textElement)) {
							groups.get(textElement).addForColumn(i);
						}
						else {
							GroupInfo gi = new GroupInfo("Tabletotal" + "_" + i, GroupInfo.TYPE_TABLE_TOTAL);
							gi.addForColumn(i);
							groups.put(textElement, gi);
						}
					}

					if (found) break;
				}
			}
			i++;
		}

		List<Map<String, Object>> groupsData = new ArrayList<>();

		// populate groupsData
		i = 0;
		for (Map.Entry<JRDesignTextElement, GroupInfo> entry: groups.entrySet()) 
		{
			JRDesignTextElement textElement = entry.getKey();
			GroupInfo groupInfo = entry.getValue();

			EditTextElementData textElementData;
			textElementData = new EditTextElementData();
			textElementData.setGroupName(groupInfo.getName());
			JRDesignTextElement styledElement = resolveElementStyle(textElement, reportContext, target);
			HeaderToolbarElementUtils.copyTextElementStyle(textElementData, styledElement, locale);

			Map<String, Object> groupData = new HashMap<>();
			groupData.put("groupType", groupInfo.getType());
			groupData.put("id", groupInfo.getType() + "_" + i);
			groupData.put("groupData", textElementData);
			groupData.put("groupName", groupInfo.getName());
			groupData.put("forColumns", groupInfo.getForColumns());

			JRDesignTextField textField = textElement instanceof JRDesignTextField ? (JRDesignTextField)textElement : null;
			ConditionalFormattingData cfData = 
				textField == null 
					? null 
					: getConditionalFormattingData(
						null,
						jasperReportsContext, 
						dataset, 
						textField,
						groupInfo.getName(),
						locale,
						timeZone
						);
			
			if (cfData != null) 
			{
				groupData.put("dataType", cfData.getConditionType());
				groupData.put("conditionalFormattingData", cfData);
			}
			else
			{
				groupData.put("dataType", FilterTypesEnum.TEXT.getName());
			}

			groupsData.add(groupData);

			i++;
		}

		return groupsData;
	}

	private static ConditionalFormattingData getConditionalFormattingData(
		JRGenericPrintElement element,
		JasperReportsContext jasperReportsContext, 
		JRDesignDataset dataset,
		JRDesignTextField textField,
		String groupName,
		Locale locale,
		TimeZone timeZone
		) 
	{
		FilterTypesEnum conditionType = null;
		String conditionTypeProp;

		// only for the detail values the element will not be null
		if (element != null) {
			conditionTypeProp = element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_FILTER_TYPE);

			if (element.getPropertiesMap().containsProperty(HeaderToolbarElement.PROPERTY_COLUMN_FIELD)) {
				textField.getPropertiesMap().setProperty(HeaderToolbarElement.PROPERTY_COLUMN_FIELD, element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_FIELD));
			} else if (element.getPropertiesMap().containsProperty(HeaderToolbarElement.PROPERTY_COLUMN_VARIABLE)) {
				textField.getPropertiesMap().setProperty(HeaderToolbarElement.PROPERTY_COLUMN_VARIABLE, element.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_COLUMN_VARIABLE));
			}

		} else {
			conditionTypeProp = textField.getPropertiesMap().getProperty(HeaderToolbarElement.PROPERTY_CONDTION_TYPE);
		}


		if (conditionTypeProp == null)
		{
			JRExpression expression = textField.getExpression();
			if (expression != null)
			{
				JRExpressionChunk[] chunks = expression.getChunks();
				if (chunks != null && chunks.length == 1)
				{
					JRExpressionChunk expressionChunk = expression.getChunks()[0];
					String fieldOrVariableName = expressionChunk.getText();

					switch (expressionChunk.getType()) {
						case JRExpressionChunk.TYPE_FIELD:
							JRField field = HeaderToolbarElementUtils.getField(fieldOrVariableName, dataset);
							conditionType = HeaderToolbarElementUtils.getFilterType(field.getValueClass());
							break;

						case JRExpressionChunk.TYPE_VARIABLE:
							JRVariable variable = HeaderToolbarElementUtils.getVariable(fieldOrVariableName, dataset);
							conditionType = HeaderToolbarElementUtils.getFilterType(variable.getValueClass());
							break;

						case JRExpressionChunk.TYPE_TEXT:
						default:
					}
				}
			}
		}
		else
		{
			conditionType = FilterTypesEnum.getByName(conditionTypeProp);
		}

		ConditionalFormattingData cfd = null; 
		if (conditionType != null)
		{
			cfd = 
				HeaderToolbarElementUtils.getConditionalFormattingData(
					textField, 
					jasperReportsContext
					);
			if (cfd == null)
			{
				cfd = new ConditionalFormattingData();
				if (groupName != null)
				{
					cfd.setGroupName(groupName);
				}
				cfd.setConditionType(conditionType.getName());
			}
			
			if (conditionType.getName().equals(cfd.getConditionType()))
			{
				String conditionPattern = HeaderToolbarElementUtils.getFilterPattern(jasperReportsContext, locale, conditionType);

				if (FilterTypesEnum.NUMERIC.equals(conditionType) && DURATION_PATTERN.equals(textField.getPattern())) {
					conditionPattern = DURATION_PATTERN;
				}

				HeaderToolbarElementUtils.updateConditionalFormattingData(
					cfd,
					conditionPattern,
					locale,
					timeZone
					);
			}
			else
			{
				//FIXMEJIVE should we raise error?
			}
		}

		return cfd;
	}

	private Set<String> getFontExtensionsFontNames(JasperReportsContext jasperReportsContext) {
		Set<String> classes = new TreeSet<>(); 

		Collection<String> extensionFonts = FontUtil.getInstance(jasperReportsContext).getFontNames();
		for (Iterator<String> it = extensionFonts.iterator(); it.hasNext();) {
			String fname = it.next();
			classes.add(fname);
		}

		return classes;
	} 

	private Set<String> getSystemFontNames(JasperReportsContext jasperReportsContext) {
		Set<String> fontExtensionsFontNames = getFontExtensionsFontNames(jasperReportsContext);
		Set<String> classes = new TreeSet<>();

		String[] names = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
		for (int i = 0; i < names.length; i++) {
			String name = names[i];
			if (fontExtensionsFontNames.add(name)) {
				classes.add(name);
			}
		}
		
		return classes;
	}
	
	/**
	 * Escapes a text to be used for a JSON string value.
	 * 
	 * @param text the text to escape for JSON
	 * @return the escaped text if not null
	 */
	private static String escapeJSONString(String text)
	{
		if (text == null)
		{
			return null;
		}
		
		// using Jackson's string quote method
		char[] escapedChars = JsonStringEncoder.getInstance().quoteAsString(text);
		if (text.contentEquals(CharBuffer.wrap(escapedChars)))
		{
			// nothing changed
			return text;
		}
		
		return String.valueOf(escapedChars);
	}

}
