/*
 * Copyright (C) 2005 - 2011 Jaspersoft Corporation. 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 free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License  as
 * published by the Free Software Foundation, either version 3 of  the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero  General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public  License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.jaspersoft.jasperserver.war.cascade.token;

import java.util.*;

import com.jaspersoft.jasperserver.war.dto.RuntimeInputControlWrapper;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.OrderedMap;
import org.apache.commons.collections.map.LinkedMap;
import org.apache.log4j.Logger;
import org.directwebremoting.WebContextFactory;
import org.springframework.context.i18n.LocaleContextHolder;

import com.jaspersoft.jasperserver.api.common.domain.ExecutionContext;
import com.jaspersoft.jasperserver.api.engine.common.service.EngineService;
import com.jaspersoft.jasperserver.api.engine.jasperreports.service.impl.EngineServiceImpl;
import com.jaspersoft.jasperserver.api.engine.jasperreports.util.DataSourceServiceFactory;
import com.jaspersoft.jasperserver.api.metadata.common.domain.InputControl;
import com.jaspersoft.jasperserver.api.metadata.common.domain.Query;
import com.jaspersoft.jasperserver.api.metadata.common.domain.Resource;
import com.jaspersoft.jasperserver.api.metadata.common.domain.ResourceReference;
import com.jaspersoft.jasperserver.api.metadata.common.service.RepositoryService;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.domain.ReportDataSource;
import com.jaspersoft.jasperserver.api.metadata.jasperreports.service.ReportDataSourceService;
import com.jaspersoft.jasperserver.war.cascade.ControlLogic;
import com.jaspersoft.jasperserver.war.cascade.EventEnvelope;
import com.jaspersoft.jasperserver.war.cascade.EventOption;
import com.jaspersoft.jasperserver.war.cascade.cache.SessionCache;
import com.jaspersoft.jasperserver.war.common.JasperServerUtil;
import com.jaspersoft.jasperserver.war.action.ReportParametersAction;

import static com.jaspersoft.jasperserver.war.action.ReportParametersUtils.setInputControlParameterValue;
import static com.jaspersoft.jasperserver.war.action.ReportParametersAction.*;

/**
 * TokenControlLogic
 * @author jwhang
 * @version $Id: TokenControlLogic.java 20446 2011-05-05 12:51:04Z afomin $
 * Singleton spring service to evaluate sets of controls.
 */

public class TokenControlLogic implements ControlLogic {

    //same as ReportParametersAction.
    private static final String COLUMN_VALUE_SEPARATOR = " | ";
    private static final int COLUMN_VALUE_SEPARATOR_LENGTH = COLUMN_VALUE_SEPARATOR.length();
    public static final String C_DASHBOARD_PREFIX = "input_";
    private static Logger log = Logger.getLogger(TokenControlLogic.class);

    private RepositoryService repositoryService;
    private EngineService engine;
    private DataSourceServiceFactory dsFactories;
    private FilterResolver filterResolver;

    public RepositoryService getRepositoryService() {
        return this.repositoryService;
    }

    public void setRepositoryService(RepositoryService repository) {
        this.repositoryService = repository;
    }

    public EngineService getEngine() {
        return this.engine;
    }

    public void setEngine(EngineService engine) {
        this.engine = engine;
    }

    public DataSourceServiceFactory getdataSourceServiceFactories() {
        return this.dsFactories;
    }

    public void setdataSourceServiceFactories(DataSourceServiceFactory dsFactories) {
        this.dsFactories = dsFactories;
    }

    public FilterResolver getFilterResolver() {
        return filterResolver;
    }

    public void setFilterResolver(FilterResolver filterResolver) {
        this.filterResolver = filterResolver;
    }

    protected ExecutionContext getExecutionContext() {
		return engine.getRuntimeExecutionContext();
    }

    // initial page load event.
    public List<EventEnvelope> initialize(String reportUri, List<EventEnvelope> envelopes, SessionCache cache) {
        return handleEvents(reportUri, envelopes, cache);
    }

    protected void replaceSubstitutes(List<EventEnvelope> envelopes, Map defaultValues) {
        for (EventEnvelope env : envelopes) {
            for (EventOption opt : env.getOptionsList()) {
                if (NULL_SUBSTITUTE.equals(opt.getValue()))  {
                    opt.setValue(null);
                    if (env.getControlType() == InputControl.TYPE_SINGLE_SELECT_QUERY || env.getControlType() == InputControl.TYPE_SINGLE_SELECT_QUERY_RADIO ) {
                        env.setControlValue(ReportParametersAction.NULL_SUBSTITUTE);
                    }
                } else if (NOTHING_SUBSTITUTE.equals(opt.getValue())) {
                    opt.setValue(defaultValues.get(env.getControlName()) != null
                            ? defaultValues.get(env.getControlName()).toString()
                            : null);
                    if (env.getControlType() == InputControl.TYPE_SINGLE_SELECT_QUERY || env.getControlType() == InputControl.TYPE_SINGLE_SELECT_QUERY_RADIO ) {
                        env.setControlValue(ReportParametersAction.NOTHING_SUBSTITUTE);
                    }
                }
            }
        }
    }

    protected void replaceSubstitutesBack(List<EventEnvelope> envelopes) {
        for (EventEnvelope env : envelopes) {
            for (EventOption opt : env.getOptionsList()) {
                if (NULL_SUBSTITUTE_LABEL.equals(opt.getLabel()))  {
                    opt.setValue(NULL_SUBSTITUTE);
                } else if (NOTHING_SUBSTITUTE_LABEL.equals(opt.getLabel())) {
                    opt.setValue(NOTHING_SUBSTITUTE);
                }
            }
        }
    }

    // user interaction with cascading input screen.
    public List<EventEnvelope> handleEvents(String reportUri, List<EventEnvelope> envelopes, SessionCache cache) {
        boolean isDashboard = false;

        //check for existence of input_ named control.
        for (EventEnvelope env : envelopes){
            if (env.getControlName().startsWith(C_DASHBOARD_PREFIX)) {
                isDashboard = true;
                removePrefix(C_DASHBOARD_PREFIX, env);
            }
        }

        //HashMap<String,ControlMapWrapper> persistentControlMap = new HashMap<String,ControlMapWrapper>();
        // This is called but the results are not used
        //populateControlStates(envelopes, persistentControlMap);

        //Get defaults
        Map<String, Object> defaultValues = new HashMap<String, Object>();
        if (!("".equals(reportUri))) {
            ExecutionContext context = JasperServerUtil.getExecutionContext(LocaleContextHolder.getLocale());
            defaultValues = engine.getReportInputControlDefaultValues(context, reportUri, null);
        }

        //Replace Null and Nothing substitutes in envelopes.
        replaceSubstitutes(envelopes, defaultValues);

        //Collect parameters for later queries
        Map<String,Object> parameterValues = getParameterValues(envelopes);

        //Add parameters if hidden controls
        defaultValues.putAll(parameterValues);
        parameterValues = defaultValues;

        // evaluate each control in order and resolve with whatever populated lists are available.
        for (EventEnvelope queryEnvelope : envelopes){
            processEnvelope(reportUri, cache, parameterValues, queryEnvelope);
        }

        replaceSubstitutesBack(envelopes);

        if (isDashboard) { //return control names to normal.
            for (EventEnvelope env : envelopes){
                addPrefix(C_DASHBOARD_PREFIX, env);
            }
        }

        return envelopes;
    }

    protected void processEnvelope(String reportUri, SessionCache cache, Map<String, Object> parameterValues, EventEnvelope queryEnvelope) {
        // check if this control is standard single, multi and query driven.  If not, take no action.
        int controlType = queryEnvelope.getControlType();

        if (controlType == InputControl.TYPE_MULTI_SELECT_QUERY ||
                controlType == InputControl.TYPE_SINGLE_SELECT_QUERY ||
                controlType == InputControl.TYPE_SINGLE_SELECT_QUERY_RADIO ||
                controlType == InputControl.TYPE_MULTI_SELECT_QUERY_CHECKBOX) {
            executeQuery(reportUri, parameterValues, queryEnvelope, cache);
        } else {
            queryEnvelope.setPermanent(true);
        }
    }

    public List<EventEnvelope> autoPopulate(String reportUri, List<EventEnvelope> envelopes, String lookupKey, SessionCache cache) {
        //process envelopes as a regular event to process alternate entries.
        return handleEvents(reportUri, envelopes, cache);
    }

    // methods to support dashboards and its behavior of adding prefixes.
    protected void addPrefix(String prefixString, EventEnvelope env) {
        env.setControlName(prefixString + env.getControlName());
    }

    protected Map<String,Object> getParameterValues(List<EventEnvelope> envelopes) {
        Map<String,Object> parameterValues = new HashMap<String,Object>();

        for (EventEnvelope travEnvelope : envelopes){
            parameterValues.put(travEnvelope.getControlName(), resolveStateForParameter(travEnvelope));
        }

        return parameterValues;
    }

    protected void removePrefix(String prefixString, EventEnvelope env) {
        String currentName = env.getControlName();
        currentName = currentName.substring(prefixString.length());
        env.setControlName(currentName);
    }


    // turns an envelope state into something comparable to other envelopes.
    protected Object resolveStateForParameter(EventEnvelope envelope) {
        List<EventOption> selections = envelope.getOptionsList();
        if (isMultiOption(envelope)) {
            List<String> selectionsAsParameter = new ArrayList<String>(selections.size());
            for (EventOption eo : selections){
                if (eo.isSelected()) {
                    selectionsAsParameter.add(eo.getValue());
                }
            }
            return selectionsAsParameter;
        } else {
            Object value = null;
            for (EventOption eo : selections){
                if (eo.isSelected()) {
                    value = eo.getValue();
                    break;
                }
            }
            return value;
        }
    }

    // read all the incoming envelopes and map them to a structure aware of next/previous.
    // This is called but the results are not used
    protected void populateControlStates(List<EventEnvelope> envelopes,
            HashMap<String, ControlMapWrapper> persistentControlMap){

        // iterate through each control.
        for (int t = 0; t < envelopes.size(); t++){
            EventEnvelope currentEnv = envelopes.get(t);

            // record the control.
            ControlMapWrapper cmw = null;
            if (t > 0) { // record the parent if not the first control.
                ControlMapWrapper prevWrap = persistentControlMap.get(envelopes.get(t-1).getResourceUriPrefix());
                cmw = new ControlMapWrapper(envelopes.get(t), prevWrap);
                // notify parent, to update nextControl reference.
                persistentControlMap.put(currentEnv.getResourceUriPrefix(), cmw);
                prevWrap.setNextControl(cmw);
            } else { //first entry.
                cmw = new ControlMapWrapper(envelopes.get(t));
                persistentControlMap.put(currentEnv.getResourceUriPrefix(), cmw);
            }
        }
    }

    // execute a control's query and retrieve the new envelope state.
    protected void executeQuery(String reportUri, Map parameterValues, EventEnvelope envelope, SessionCache cache){
        if (envelope == null) {
            return;
        }

        //get a handle to the repository element and get query.
        String rawQueryString = "";

        String resourceUri = envelope.getResourceUriPrefix();
        ExecutionContext context = JasperServerUtil.getExecutionContext(LocaleContextHolder.getLocale());
        context = EngineServiceImpl.getRuntimeExecutionContext(context);

        Resource resource = repositoryService.getResource(context, resourceUri);

        if (!(resource instanceof InputControl)) {
            return;
        }

        InputControl control = (InputControl) resource;

        String valueColumn = control.getQueryValueColumn();
        String[] visibleColumns = control.getQueryVisibleColumns();

        ResourceReference queryReference = control.getQuery();
        ResourceReference dataSourceRef = null;

        Object queryResObj = null;

        envelope.setMandatory(control.isMandatory());
        if (queryReference == null) {
            envelope.setPermanent(true);
            return;
        }

        queryResObj = getFinalResource(context, queryReference);

        if (queryResObj instanceof Query) {
            rawQueryString = ((Query )queryResObj).getSql();
            dataSourceRef = ((Query) queryResObj).getDataSource();
        }

        List wrappers = getWrappers(envelope.getWrappersUUID());
        if (wrappers != null) {
            for (RuntimeInputControlWrapper wrapper : (List<RuntimeInputControlWrapper>) wrappers) {
                if (wrapper.getInputControl().getName().equals(envelope.getControlName())) {
                    Object value = null;
                    if (isMultiOption(envelope)) {
                        value = new ArrayList();
                        for (EventOption eventOption : envelope.getOptionsList()) {
                            if (eventOption.isSelected()) {
                                ((List) value).add(eventOption.getValue());
                            }
                        }
                    } else {
                        for (EventOption eventOption : envelope.getOptionsList()) {
                            if (eventOption.isSelected()) {
                                value = eventOption.getValue();
                                break;
                            }
                        }
                    }
                    setInputControlParameterValue(wrapper, value, repositoryService);
                }
            }
        }

        if (dataSourceRef == null) {
            envelope.setPermanent(true);
            return;
        }
        
        /**
         * DomainFilterResolver needs access to the domain schema, which it can get from 
         * the param map. FilterCore doesn't need this, and it would allocate a connection
         * that's not needed.
         */
    	ReportDataSource dataSource = (ReportDataSource) getFinalResource(context, dataSourceRef);
        if (getFilterResolver().paramTestNeedsDataSourceInit(dataSource)) {
        	ReportDataSourceService dataSourceService = engine.createDataSourceService(dataSource);
        	dataSourceService.setReportParameterValues(parameterValues);
        }

        // before trying to resolve query, check to see if the raw query is a regular SQL query.
        if (!getFilterResolver().hasParameters(rawQueryString, parameterValues)){
            envelope.setPermanent(true);
            return;
        }

        // check to see if we have all the parameters for the query
        //
        // resolvedQuery will be null if not, otherwise it will be a
        // textual rep of the query, suitable as a key for the cache
        //
        // may add to parameterValues if needed
        Object queryKey = getFilterResolver().getCacheKey(rawQueryString, parameterValues);

        //check to see if the query can be fully resolved now
        if (queryKey == null) {
            return;
        }

        //all conditions met, assert changes to the envelope.

        String lookupKey = reportUri + "|" + control.getName() + "|" + queryKey;
        OrderedMap inputData = null;

        //attempt retrieval from cache first.
        if (cache != null) {
            Object cacheInputData = cache.getCacheInfo(this.getClass(), lookupKey);
            if (log.isDebugEnabled()) {
            	log.debug("query result " + (cacheInputData == null ? "not found" : "found    ") + " for key " + lookupKey);
            }
            if (cacheInputData != null && cacheInputData instanceof LinkedMap){
                inputData = (LinkedMap) cacheInputData;
            }
        }

        //no value assigned, run query.
        if (inputData == null) {
            //no entry in cache, run query and add to cache.
            OrderedMap results = engine.executeQuery(null, queryReference, valueColumn, visibleColumns, dataSourceRef, parameterValues);
            //same iteration as ReportParametersAction.
            inputData = new LinkedMap();

            for (Iterator it = results.entrySet().iterator(); it.hasNext();) {
                Map.Entry entry = (Map.Entry) it.next();
                Object keyValue = entry.getKey();
                String[] columnValues = (String[]) entry.getValue();

                String columnValuesString = "";
                if (columnValues != null && columnValues.length > 0) {
                    StringBuffer visibleColumnBuffer = new StringBuffer();
                    for (int i = 0; i < columnValues.length; i++) {
                        visibleColumnBuffer.append(COLUMN_VALUE_SEPARATOR);
                        visibleColumnBuffer.append(columnValues[i] != null ? columnValues[i] : "");
                    }
                    // trim because javascript dwr loses that whitespaces and values don't match in UI - selection in control is lost
                    columnValuesString = visibleColumnBuffer.substring(COLUMN_VALUE_SEPARATOR_LENGTH).trim();
                }
                inputData.put( (keyValue == null ? null : keyValue.toString()), new Object[] {keyValue, columnValuesString});
            }
            if (cache != null){
                cache.setCacheInfo(this.getClass(), lookupKey, inputData);
            }
        }
        
        // Update InputControl wrappers
        // #17788 fix
        boolean isQueryResultsDifferent = true;
        if (wrappers != null) {
            for (RuntimeInputControlWrapper wrapper : (List<RuntimeInputControlWrapper>) wrappers) {
                if (wrapper.getInputControl().getName().equals(envelope.getControlName())) {
                    isQueryResultsDifferent = isQueryResultsDifferent(wrapper.getQueryResults(), inputData);
                    wrapper.setQueryResults(inputData);
                }
            }
        }

        //clear envelope.
        List<EventOption> opList = new ArrayList<EventOption>();

        // Save previous selections (values) for testing

        Set<String> previousSelections = new HashSet<String>();

        for (EventOption eo : envelope.getOptionsList()) {
            if (eo.isSelected()) {
                previousSelections.add(eo.getValue());
            }
        }


        if (!control.isMandatory()
                && control.getType() == InputControl.TYPE_SINGLE_SELECT_QUERY || control.getType() == InputControl.TYPE_SINGLE_SELECT_LIST_OF_VALUES) {
            // always add an unselected entry to single select lists
            opList.add(new EventOption(NOTHING_SUBSTITUTE_LABEL, NOTHING_SUBSTITUTE, previousSelections.contains(NOTHING_SUBSTITUTE)));
        }

        //populate envelope with results.
        boolean setFirstInListSelected = true;
        MapIterator resultIterator = inputData.mapIterator();
        while (resultIterator.hasNext()){
            resultIterator.next();
            Object [] valSet = (Object []) resultIterator.getValue();

            String valString;
            String labelString;
            boolean previouslySelected;
            if (valSet[0] == null) {
                valString = NULL_SUBSTITUTE;
                labelString = NULL_SUBSTITUTE_LABEL;
                previouslySelected = previousSelections.contains(null);
            } else {
                valString = String.valueOf(valSet[0]);
                labelString = String.valueOf(valSet[1]);
                previouslySelected = previousSelections.contains(valString);
            }
            if (previouslySelected) {
                setFirstInListSelected = false;
            }

            opList.add(new EventOption(labelString, valString, previouslySelected));
        }

        // if nothing selected - set first item selected by default but only
        // if query results had been changed
        if (setFirstInListSelected) {
            // wrappers are null in dashboard, so isQueryResultsDifferent always will be true in dashboard
            if (!opList.isEmpty() && control.isMandatory() && isQueryResultsDifferent) {
                opList.get(0).setSelected(true);
            }
        }

        envelope.setOptionsList(opList);

        // reset selections
        if (isMultiOption(envelope)) {
            if (opList.size() > 0) {
                parameterValues.put(control.getName(), resolveStateForParameter(envelope));
            }
        } else {
            // single select

            // if something is already selected and in the returned list, keep it.
            // Otherwise, reset.
            if (envelope.getControlValue().length() > 0 && opList.size() > 0){
                //check for this value.
                boolean overwriteValue = true;
                for (EventOption op : opList){
                    if (op.getValue().equalsIgnoreCase(envelope.getControlValue())) {
                        op.setSelected(true);
                        overwriteValue = false;
                        break;
                    }
                }
                if (overwriteValue) {
                    /*
                        Fix for #18405 - set control value to the first option.
                        Setting empty string is no good since it leads to selectedIndex == -1 for WebKit,
                        causing theControl.options[theControl.selectedIndex] to fail.
                     */
                    envelope.setControlValue(opList.get(0).getValue());
                    //#17788 fix
                    parameterValues.put(control.getName(), opList.get(0).getValue());
                } else {
                    // The old value is in the list, so set the parameter so
                    // other parameters can use it
                    parameterValues.put(control.getName(), envelope.getControlValue());
                }
            // if parameter doesn't have default value
            } else if (envelope.getControlValue().length() == 0 && opList.size() > 0) {
                if (control.isMandatory()) {
                    envelope.setControlValue(opList.get(0).getValue());
                    parameterValues.put(control.getName(), opList.get(0).getValue());
                }
            } else {
                envelope.setControlValue("");
            }
        }
    }

    private boolean isQueryResultsDifferent(Map oldQueryResults, Map newQueryResults) {
        if (oldQueryResults.size() != newQueryResults.size()) {
            return true;
        }
        
        for (Object key: oldQueryResults.keySet()) {
            Object [] oldValSet = (Object []) oldQueryResults.get(key);
            Object [] newValSet = (Object []) newQueryResults.get(key);
            if (newValSet == null || oldValSet.length != newValSet.length) {
                return true;
            }
            for (int i = 0; i < oldValSet.length; i++) {
                if (oldValSet[i] == null ^ newValSet[i] == null) {
                    return true;
                } else if (oldValSet[i] != null && !oldValSet[i].equals(newValSet[i])) {
                    return true;
                }
            }
        }

        return false;
    }

    private List getWrappers(String wrappersUUID) {
        Map wrappersMap = (Map) WebContextFactory.get().getSession().getAttribute(
            ReportParametersAction.INPUTWRAPPERS_ATTR);
        if (wrappersMap != null) {
            return (List) wrappersMap.get(wrappersUUID);
        }

        return null;
    }

    public static boolean isMultiOption(EventEnvelope envelope) {
        int controlType = envelope.getControlType();
        return controlType == InputControl.TYPE_MULTI_SELECT_LIST_OF_VALUES ||
                controlType == InputControl.TYPE_MULTI_SELECT_QUERY ||
                controlType == InputControl.TYPE_MULTI_SELECT_LIST_OF_VALUES_CHECKBOX ||
                controlType == InputControl.TYPE_MULTI_SELECT_QUERY_CHECKBOX;
    }
    
	protected Resource getFinalResource(ExecutionContext context, ResourceReference res) {
		Resource finalRes;
		if (res.isLocal()) {
			finalRes = res.getLocalResource();
		} else {
			finalRes = repositoryService.getResource(context, res.getReferenceURI());
		}
		return finalRes;
	}


}
