/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.integrator.chart;

import java.io.CharArrayReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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.TimeZone;

import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;

import com.queplix.core.client.app.vo.ChartFieldDataRequest;
import com.queplix.core.client.app.vo.CheckBoxData;
import com.queplix.core.client.app.vo.DateFieldData;
import com.queplix.core.client.app.vo.EntityData;
import com.queplix.core.client.app.vo.EntityReferenceData;
import com.queplix.core.client.app.vo.FieldData;
import com.queplix.core.client.app.vo.FieldMeta;
import com.queplix.core.client.app.vo.ListboxFieldData;
import com.queplix.core.client.app.vo.MemoFieldData;
import com.queplix.core.client.app.vo.SubsetData;
import com.queplix.core.client.app.vo.TextareaFieldData;
import com.queplix.core.client.app.vo.TextboxFieldData;
import com.queplix.core.client.app.vo.chart.ChartDetails;
import com.queplix.core.client.app.vo.chart.ChartMeta;
import com.queplix.core.client.app.vo.chart.ChartModel;
import com.queplix.core.client.app.vo.chart.ChartOrientation;
import com.queplix.core.client.app.vo.chart.ChartType;
import com.queplix.core.client.app.vo.chart.DefaultChartModel;
import com.queplix.core.integrator.ActionContext;
import com.queplix.core.integrator.entity.EntityFacade;
import com.queplix.core.integrator.entity.EntitySerializeHelper;
import com.queplix.core.integrator.entity.EntityViewHelper;
import com.queplix.core.integrator.entity.RequestProperties;
import com.queplix.core.integrator.entity.EntityViewHelper.FieldsModificator;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.integrator.security.SecurityHelper;
import com.queplix.core.integrator.security.User;
import com.queplix.core.modules.config.utils.EntityHelper;
import com.queplix.core.modules.eql.EQLObject;
import com.queplix.core.modules.eql.EQLRes;
import com.queplix.core.modules.eql.EQLResRecord;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.eqlext.jxb.gr.Chart;
import com.queplix.core.modules.eqlext.jxb.gr.ChartCategoryField;
import com.queplix.core.modules.eqlext.jxb.gr.ChartDataField;
import com.queplix.core.modules.eqlext.jxb.gr.ChartEfieldType;
import com.queplix.core.modules.eqlext.jxb.gr.ChartParam;
import com.queplix.core.modules.eqlext.jxb.gr.ChartParams;
import com.queplix.core.modules.eqlext.jxb.gr.ChartReq;
import com.queplix.core.modules.eqlext.jxb.gr.Req;
import com.queplix.core.modules.eqlext.jxb.gr.ReqField;
import com.queplix.core.modules.eqlext.jxb.gr.ReqFilter;
import com.queplix.core.modules.eqlext.jxb.gr.ReqFilters;
import com.queplix.core.modules.eqlext.jxb.gr.ReqFiltersTypeItem;
import com.queplix.core.modules.eqlext.jxb.gr.Reqs;
import com.queplix.core.modules.eqlext.jxb.gr.types.AggregateSType;
import com.queplix.core.modules.eqlext.jxb.gr.types.ConditionSType;
import com.queplix.core.modules.eqlext.jxb.gr.types.OrderDirectionSType;
import com.queplix.core.modules.eqlext.utils.ExtDateParser;
import com.queplix.core.modules.jeo.gen.ChartObject;
import com.queplix.core.modules.jeo.gen.ChartObjectHandler;
import com.queplix.core.utils.DateHelper;
import com.queplix.core.utils.StringHelper;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;
import com.queplix.core.utils.xml.XMLHelper;

/**
 * Manages all chart-related data operations like add/edit/remove/build chart data
 * @author Michael Trofimov
 */
public class ChartDataManager {

    private static final AbstractLogger logger = Log.getLog(
            ChartDataManager.class);

    private static final String PARAM_WIDTH = "width";
    private static final String PARAM_HEIGHT = "height";
    private static final String PARAM_COLORS = "colors";

    private static final ChartOrientation DEFAULT_ORIENTATION
            = ChartOrientation.HORIZONTAL;
    private static final int DEFAULT_HEIGHT = 300;
    private static final int DEFAULT_WIDTH = 400;

    private static final Map<Long, ChartObject> chartsCache
            = Collections.synchronizedMap(new HashMap<Long, ChartObject>());

    private ChartDataManager() {
    }

    public static void clearChartsCache(){
        chartsCache.clear();
    }

    public static ChartDetails getChartDetails(
            LogonSession ls, ActionContext ctx, long id)
                    throws EQLException {

        ChartObject obj = getChartObject(ls, ctx, id);
        if(obj == null) {
            return null;
        }

        return transformJEOToDetails(ls, ctx, obj);
    }

    public static ChartModel getChart(
            LogonSession ls, ActionContext ctx, long id)
                    throws EQLException {

        ChartObject obj = getChartObject(ls, ctx, id);
        if(obj == null) {
            return null;
        }

        return new DefaultChartModel(transformJEOToMeta(ls, ctx, obj));
    }

    /**
     * Incapsulates all data to build chart's image
     * and provides FieldData objects for each value
     * of category field (for drilldown functionality)
     */
    public static class IntegratedChartData {
        DefaultCategoryDataset dataSet = new DefaultCategoryDataset();
        Map<Comparable, FieldData> fieldData
                = new HashMap<Comparable, FieldData>();

        public CategoryDataset getCategoryDataset() {
            return dataSet;
        }

        public FieldData getFieldData(int column) {
            return fieldData.get(dataSet.getColumnKey(column));
        }
    }

    // TODO Glue method
    public static ChartMeta buildChartMeta(
            LogonSession ls, ActionContext ctx, ChartFieldDataRequest req)
                    throws EQLException {

        String chartName = req.getChartName();
        ChartObject obj = ChartObjectHandler.findByName(
                ctx.getJEOManager(), ls, chartName);
        if(obj == null) {
            logger.WARN("Can't retrieve chart JEO by name=" + chartName);
            return null;
        }

        if (!chartsCache.containsKey(obj.getChart_id())) {
            chartsCache.put(obj.getChart_id(), obj);
        }
        
        ChartMeta meta = transformJEOToMeta(ls, ctx, obj);
        
        // Sets up chart's type
        ChartType type = req.getChartType();
        if (type != null)
            meta.setType(type);
        
        // Sets up filters from ChartFieldDataRequest to ChartMeta
        Map/*<String, List<FieldData>*/ filterMap = new HashMap();
        EntityData[] filters = req.getFilters();
        for (int i = 0; i < filters.length; i++) {
            EntityData entityData = filters[i];
            if (entityData != null){
                String fakeFormId = entityData.getEntityID()
                        + EntityHelper.FORM_NAME_SEPARATOR + entityData.getEntityID();
                List filterList = (List) filterMap.get(fakeFormId);
                if (filterList == null) {
                    filterList = new ArrayList();
                    filterMap.put(fakeFormId, filterList);
                }
                FieldData[] fieldDatas = entityData.getNotEmptyFields();
                filterList.addAll(Arrays.asList(fieldDatas));
            }
        }
        meta.setFilters(filterMap);
        
        meta.setEmpty(req.isEmpty());

        return meta;
    }
    
    public static IntegratedChartData getChartData(
            LogonSession ls, ActionContext ctx, ChartMeta meta)
                    throws EQLException {

        if(meta == null) {
            throw new IllegalArgumentException("meta can't be a null");
        }

        if(meta.getID() == null) {
            throw new IllegalArgumentException("Unspecified chart id");
        }

        ChartObject obj = getChartObject(ls, ctx, meta.getID());
        if(obj == null) {
            return null;
        }

        IntegratedChartData data = new IntegratedChartData();
        
        if(meta.isEmpty())
            return data;

        Chart chart = transformJEOToJXB(obj);
        ChartReq chartReq = chart.getChartReq();

        ChartCategoryField categoryField = chartReq.getChartCategoryField();
        // TODO cache field meta
        FieldMeta categoryMeta = EntityViewHelper.getMetaForField(
                categoryField.getEntity(), categoryField.getName(), false, ls,
                ctx);

        Reqs reqs = createReqsFromChartReq(ls, ctx, chartReq, meta);
        Req req = reqs.getReq();
        ReqField categoryReqField = createReqFieldFromCategoryField(
                chartReq.getChartCategoryField());

        for(ChartDataField dataField : chartReq.getChartDataField()) {
            ReqField dataReqField = createReqFieldFromDataField(dataField);
            req.addReqField(categoryReqField);
            req.addReqField(dataReqField);

            EQLRes res = ctx.getRecordsManager().process(reqs, ls).getEQLRes();
            if(res == null) {
                continue;
            }

            fillChartData(ls, categoryMeta, dataField, res, data);

            req.clearReqField();
        }

        return data;
    }

    private static synchronized ChartObject getChartObject(
            LogonSession ls, ActionContext ctx, long id)
                    throws EQLException {

        ChartObject obj = chartsCache.get(id);
        if (obj == null) {
            obj = ChartObjectHandler.findByID(
                    ctx.getJEOManager(), ls, id);

            if(obj == null) {
                logger.WARN("Can't retrieve chart JEO by ID=" + id);
                return null;
            }

            chartsCache.put(id, obj);
        }

        return obj;
    }

    private static void fillChartData(LogonSession ls, FieldMeta categoryMeta,
                                      ChartDataField dataField, EQLRes res,
                                      IntegratedChartData data) {

        String dataName = EntityHelper.getFieldId(
                dataField.getEntity(), dataField.getName());

        for(int i = 0; i < res.size(); i++) {
            EQLResRecord record = res.getRecord(i);

            // Process category field
            EQLObject valueEqlObj = record.getEQLObject(0);
            Object valueObj = valueEqlObj.getObject();

            EQLObject textEqlObj = record.getEQLListObject(0);
            Object textObj = textEqlObj != null ? textEqlObj.getObject():null;

            // Gets category field key
            String categoryName = textObj != null ? textObj.toString()
                    :(valueObj != null ? valueObj.toString():"Empty");

            data.fieldData.put(categoryName,
                    createFieldData(ls, categoryMeta, valueObj, textObj));

            data.dataSet.addValue(
                    record.getResCell(1).getNumber(),
                            dataName, categoryName);
        }
    }
    
    private static Reqs createReqsFromChartReq(
    		LogonSession ls, ActionContext ctx, ChartReq chartReq, ChartMeta meta) {

        RequestProperties requestProps = new RequestProperties(
                false,  // doCount = false, 
                0,      // page = 0, first page
                -1,     // pageSize = -1, retrieve all records 
                null);  // no sorting

        Reqs reqs = EntityFacade.createRequest(requestProps);

        reqs.setReqFilters(createReqFiltersFromFilters(ls, ctx, meta));
        reqs.setEqlFilters(chartReq.getEqlFilters());

        logger.DEBUG("Created chart request: " + reqs);

        return reqs;
    }

    private static ReqField createReqFieldFromDataField(
            ChartDataField dataField) {
        ReqField reqField = createReqFieldFromChartEfield(dataField);
        reqField.setAggregate(AggregateSType.COUNT);
        return reqField;
    }

    private static ReqField createReqFieldFromCategoryField(
            ChartCategoryField categoryField) {
        ReqField reqField = createReqFieldFromChartEfield(categoryField);
        OrderDirectionSType sortDir = categoryField.getSortdir();
        if(sortDir != null) {
            reqField.setSort(true);
            reqField.setSortdir(sortDir);
        }
        return reqField;
    }

    private static ReqField createReqFieldFromChartEfield(
            ChartEfieldType chartEfield) {
        ReqField reqField = new ReqField();
        reqField.setEntity(chartEfield.getEntity());
        reqField.setName(chartEfield.getName());
        return reqField;
    }

    public static ChartModel[] getSystemCharts(LogonSession ls,
                                               ActionContext ctx)
            throws EQLException {

        List<ChartObject> objs = ChartObjectHandler.findSystemCharts(
                ctx.getJEOManager(), ls);
        if(objs == null || objs.isEmpty()) {
            return new ChartModel[0];
        }

        ChartModel[] models = new ChartModel[objs.size()];
        for(int i = 0; i < objs.size(); i++) {
            models[i] = new DefaultChartModel(transformJEOToMeta(ls, ctx,
                    objs.get(i)));
        }

        return models;
    }

    private static ChartMeta transformJEOToMeta(LogonSession ls,
                                                ActionContext ctx,
                                                ChartObject obj) {
        return transformJEOToDetails(ls, ctx, obj).toMeta();
    }

    private static ChartDetails transformJEOToDetails(LogonSession ls,
                                                      ActionContext ctx,
                                                      ChartObject obj) {
        if(obj == null) {
            throw new IllegalArgumentException("obj can't be a null");
        }

        char[] body = obj.getBody();
        if(body == null || body.length == 0) {
            throw new IllegalStateException("Chart body can't be a null");
        }

        Chart chart = transformJEOToJXB(obj);

        ChartDetails details = new ChartDetails(obj.getChart_id());

        ChartReq chartReq = chart.getChartReq();

        details.setCategoryFieldFormId(
                chartReq.getChartCategoryField().getFormid());

        List<String> dataFieldFormIds = new ArrayList<String>();
        for(int i = 0; i < chartReq.getChartDataFieldCount(); i++) {
            dataFieldFormIds.add(chartReq.getChartDataField(i).getFormid());
        }
        details.setDataFieldFormIds(dataFieldFormIds);

        ChartParams params = chart.getChartParams();
        details.setType(ChartType.valueOf(params.getType().toString()));
        details.setTitle(params.getTitle());

        // TODO move default chart's orientation to the system properties
        details.setOrientation(DEFAULT_ORIENTATION);
        if(params.getOrientation() != null) {
            details.setOrientation(
                    ChartOrientation.valueOf(
                            params.getOrientation().toString()));
        }

        // TODO move default chart's width to the system properties
        details.setWidth(DEFAULT_WIDTH);
        String width = getChartParameter(params, PARAM_WIDTH);
        if(width != null) {
            details.setWidth(Integer.parseInt(width));
        }

        // TODO move default chart's height to the system properties
        details.setHeight(DEFAULT_HEIGHT);
        String height = getChartParameter(params, PARAM_HEIGHT);
        if(height != null) {
            details.setHeight(Integer.parseInt(height));
        }

        String colors = getChartParameter(params, PARAM_COLORS);
        if(colors != null) {
            details.setColors(colors);
        }

        details.setFilters(createFiltersFromReqFilters(
                ls, ctx, chart.getChartReq().getReqFilters()));

        return details;
    }

    /**
     * Mapping ReqFilters -> FieldData[] is partially supported now,
     * nested ReqFilters are not supported.
     *
     * @return map
     */
    private static Map<String, List<FieldData>> createFiltersFromReqFilters(
            LogonSession ls, ActionContext ctx, ReqFilters filters) {

        if(filters == null || filters.getReqFiltersTypeItemCount() == 0) {
            return new HashMap<String, List<FieldData>>();
        }

        if(!ConditionSType.AND.equals(filters.getType())) {
            throw new IllegalStateException("Only req-filters/@type='"
                    + ConditionSType.AND + "' is supported");
        }

        Map<String, List<FieldData>> dataMap
                = new HashMap<String, List<FieldData>>();

        for(ReqFiltersTypeItem filterItem : filters.getReqFiltersTypeItem()) {
            ReqFilter filter = filterItem.getReqFilter();
            if(filter == null) {
                continue;
            }

            String formId = filter.getFormid();
            List<FieldData> data = dataMap.get(formId);
            if(data == null) {
                data = new ArrayList<FieldData>();
                dataMap.put(formId, data);
            }
            data.add(createFieldDataFromReqFilter(ls, ctx, filter));
        }

        return dataMap;
    }
    
    @SuppressWarnings("unchecked")
    private static ReqFilters createReqFiltersFromFilters(
            LogonSession ls, ActionContext ctx, ChartMeta meta){

    	ReqFilters reqFilters = new ReqFilters();
    	List<ReqFiltersTypeItem> items = new ArrayList<ReqFiltersTypeItem>();
    	for (Iterator it = meta.getFilters().keySet().iterator(); it.hasNext();) {
            String formId = (String) it.next();
            List<FieldData> fieldDatas = (List<FieldData>) meta.getFilters().get(formId);
            if(fieldDatas == null || fieldDatas.size() == 0)
            	continue;
            
            String entity = EntityHelper.getFormEntityName(formId);
            Map<String, FieldMeta> fieldMetas
                    = EntityViewHelper.getMetaForEntity(
                            entity, FieldsModificator.FORM, false, ls, ctx);

            for (FieldData fieldData : fieldDatas) {
                ReqFilter reqFilter = EntitySerializeHelper.createEFieldFilter(
                		entity, fieldMetas.get(fieldData.getFieldID()), fieldData, ls);
                if(reqFilter == null)
                    continue;
                
                ReqFiltersTypeItem item = new ReqFiltersTypeItem();
                item.setReqFilter(reqFilter);
                items.add(item);
            }
        }
    	reqFilters.setReqFiltersTypeItem(items.toArray(new ReqFiltersTypeItem[0]));
    	return reqFilters;
    }

// TODO move methods to the EntityFacade {{{
    private static FieldData createFieldData(LogonSession ls, FieldMeta fieldMeta, Object value, Object text){
        FieldData fieldData = EntitySerializeHelper.createEmptyFieldData(fieldMeta, null);
        switch (fieldMeta.getDataType()) {
            case FieldMeta.LISTBOX:
                ListboxFieldData listboxFieldData
                        = (ListboxFieldData) fieldData;
                SubsetData subSet = listboxFieldData.getItemsSelected();
                if(value == null) {
                    subSet.setNullSelected(true);
                } else {
                    subSet.setIDSelected(((Number) value).longValue(), true);
                }

                break;
            case FieldMeta.TEXTBOX:
                TextboxFieldData textboxFieldData
                        = (TextboxFieldData) fieldData;
                textboxFieldData.setText((String) value);
                break;
            case FieldMeta.TEXTAREA:
                TextareaFieldData textareaFieldData
                        = (TextareaFieldData) fieldData;
                textareaFieldData.setText((String) value);
                break;
            case FieldMeta.CHECKBOX:
                if(value != null) {
                    CheckBoxData checkboxFieldData = (CheckBoxData) fieldData;
                    checkboxFieldData.setChecked(
                            value instanceof String && ((String) value).equals(
                                    "1"));
                }
                break;
            case FieldMeta.ENTITYREFERENCE:
                if(value != null) {
                    EntityReferenceData entityFieldData
                            = (EntityReferenceData) fieldData;
                    try {
                        entityFieldData.setSelectedRowID(
                                ((Number) value).longValue());
                    } catch (NumberFormatException e) {
                    }
                    if(text != null) {
                        entityFieldData.setSelectedFilter((String) text);
                    }
                }
                break;
// TODO implement this later
//            case FieldMeta.DATEFIELD:
//                break;
        }
        return fieldData;
    }

    private static FieldData createFieldDataFromReqFilter(LogonSession ls,
                                                          ActionContext ctx,
                                                          ReqFilter filter) {
        FieldData fieldData = null;

        FieldMeta fieldMeta = EntityViewHelper.getMetaForField(
                filter.getEntity(), filter.getName(), false, ls, ctx);
        if(fieldMeta != null) {
            String fieldName = filter.getName();
            switch(fieldMeta.getDataType()) {
                case FieldMeta.LISTBOX:
                    long[] values = new long[filter.getReqFilterValueCount()];
                    int i = 0;
                    for(String filterValue : filter.getReqFilterValue()) {
                        values[i++] = Long.valueOf(filterValue);
                    }
                    fieldData = new ListboxFieldData(fieldName, new SubsetData(
                            values));
                    break;
                case FieldMeta.TEXTBOX:
                    fieldData = new TextboxFieldData(fieldName,
                            filter.getReqFilterValue(0));
                    break;
                case FieldMeta.TEXTAREA:
                    fieldData = new TextareaFieldData(fieldName,
                            filter.getReqFilterValue(0));
                    break;
                case FieldMeta.MEMO:
                    fieldData = new MemoFieldData(fieldName,
                            filter.getReqFilterValue(0), -1);
                    break;
                case FieldMeta.CHECKBOX:
                    CheckBoxData checkboxData = new CheckBoxData(fieldName,
                            null);
                    String checkboxValue = filter.getReqFilterValue(0);
                    if(checkboxValue != null) {
                        checkboxData.setChecked(checkboxValue.equals("1"));
                    }

                    fieldData = checkboxData;
                    break;
                case FieldMeta.ENTITYREFERENCE:
                    EntityReferenceData entityData = new EntityReferenceData(
                            fieldName);
                    try {
                        entityData.setSelectedRowID(Long.valueOf(
                                filter.getReqFilterValue(0)));
                    } catch (NumberFormatException e) {
                        entityData.setSelectedFilter(filter.getReqFilterText(
                                0));
                    }
                    fieldData = entityData;
                    break;
                case FieldMeta.DATEFIELD:
// TODO review this code, @see EntitySerializeHelper#createFieldData()
                    Date startDate = null;
                    Date endDate = null;

                    User user = ls.getUser();
                    Locale locale = SecurityHelper.getJavaLocale(user.getCountryID(), user.getLangID());
                    TimeZone tz = SecurityHelper.getJavaTimezone(user.getTimeZoneID());
                    String datePattern = user.getDatePattern();
                    String timePattern = user.getTimePattern();
                    boolean dayPosFirst = user.isDatePositionFirst();

                    String[] rawDateRange = EntitySerializeHelper.getDatesFromString(
                            filter.getReqFilterValue(0));
                    if(rawDateRange != null) {
                        try {
                            long sysDateMls = ExtDateParser.parseDate(
                                    rawDateRange[0], dayPosFirst, locale, tz, datePattern, timePattern);
                            startDate = new Date(DateHelper.toUser(sysDateMls, tz));
                            if(rawDateRange.length > 1){
                                sysDateMls = ExtDateParser.parseDate(
                                        rawDateRange[1], dayPosFirst, locale, tz, datePattern, timePattern);
                                endDate = new Date(DateHelper.toUser(sysDateMls, tz));
                            }
                        } catch (Exception e) {
                            logger.ERROR("Couldn't create data for startDate field", e);
                        }
                    }
                    String representation
                            = EntitySerializeHelper.getDateStringRepresentation(
                                    startDate, endDate, datePattern, timePattern, locale);
                    DateFieldData dateData = new DateFieldData(fieldName, startDate, DateHelper.getNowDate(), representation);
                    if(endDate != null)
                        dateData.setEndDate(endDate);

                    fieldData = dateData;
                    break;
                case FieldMeta.ENTITYLINK:
                    break;
                case FieldMeta.HISTORY:
                    break;
                case FieldMeta.IN_FORM_GRID: // ???
                    break;
                case FieldMeta.MULTISELECT:
                    break;
            }
        } else {
            logger.WARN(
                    "Unable to find metadata for field name=" + filter.getName()
                            + ", entity=" + filter.getEntity() + ", formid="
                            + filter.getFormid());
        }

        return fieldData;
    }
//  TODO move methods to the EntityFacade }}}

    private static String getChartParameter(ChartParams params,
                                            String paramName) {
        for(ChartParam param : params.getChartParam()) {
            if(param.getName().equalsIgnoreCase(paramName)) {
                return param.getValue();
            }
        }
        return null;
    }

    private static Chart transformJEOToJXB(ChartObject jeo) {
        String bodyString = StringHelper.clearXml(
                StringHelper.unEscapeUnicode(new String(jeo.getBody())), false);
        return (Chart) XMLHelper.getParsedObject(
                Chart.class, new CharArrayReader(bodyString.toCharArray()));
    }

}
