package net.sf.jasperreports.engine.fill;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TimeZone;
import net.sf.jasperreports.engine.JRAbstractScriptlet;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRDataset;
import net.sf.jasperreports.engine.JRDefaultScriptlet;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRGroup;
import net.sf.jasperreports.engine.JRParameter;
import net.sf.jasperreports.engine.JRPropertiesHolder;
import net.sf.jasperreports.engine.JRPropertiesMap;
import net.sf.jasperreports.engine.JRQuery;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JRSortField;
import net.sf.jasperreports.engine.JRVariable;
import net.sf.jasperreports.engine.JasperCompileManager;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRSortableDataSource;
import net.sf.jasperreports.engine.design.JRDesignVariable;
import net.sf.jasperreports.engine.query.JRQueryExecuter;
import net.sf.jasperreports.engine.query.JRQueryExecuterFactory;
import net.sf.jasperreports.engine.util.JRClassLoader;
import net.sf.jasperreports.engine.util.JRQueryExecuterUtils;
import net.sf.jasperreports.engine.util.JRResourcesUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JRFillDataset implements JRDataset {
  private static final Log log = LogFactory.getLog(JRFillDataset.class);
  
  private final JRBaseFiller filler;
  
  private final JRDataset parent;
  
  private final boolean isMain;
  
  protected JRQuery query = null;
  
  private boolean useDatasourceParamValue = false;
  
  private boolean useConnectionParamValue = false;
  
  protected JRFillParameter[] parameters = null;
  
  protected Map parametersMap = null;
  
  protected JRFillField[] fields = null;
  
  protected Map fieldsMap = null;
  
  protected JRFillVariable[] variables = null;
  
  protected Map variablesMap = null;
  
  protected Set variableCalculationReqs;
  
  protected JRFillElementDataset[] elementDatasets;
  
  protected JRFillElementDataset[] origElementDatasets;
  
  protected JRFillGroup[] groups = null;
  
  protected String resourceBundleBaseName = null;
  
  protected byte whenResourceMissingType;
  
  protected String scriptletClassName = null;
  
  protected JRDataSource dataSource = null;
  
  protected Locale locale = null;
  
  protected ResourceBundle resourceBundle = null;
  
  protected TimeZone timeZone = null;
  
  protected int reportCount = 0;
  
  protected JRCalculator calculator = null;
  
  protected JRAbstractScriptlet scriptlet = null;
  
  protected Integer reportMaxCount = null;
  
  private JRQueryExecuter queryExecuter;
  
  protected JRFillDataset(JRBaseFiller filler, JRDataset dataset, JRFillObjectFactory factory) {
    factory.put(dataset, this);
    this.filler = filler;
    this.parent = dataset;
    this.isMain = dataset.isMainDataset();
    this.scriptletClassName = dataset.getScriptletClass();
    this.resourceBundleBaseName = dataset.getResourceBundle();
    this.whenResourceMissingType = dataset.getWhenResourceMissingType();
    this.query = dataset.getQuery();
    setParameters(dataset, factory);
    setFields(dataset, factory);
    setVariables(dataset, factory);
    setGroups(dataset, factory);
  }
  
  private void setParameters(JRDataset dataset, JRFillObjectFactory factory) {
    JRParameter[] jrParameters = dataset.getParameters();
    if (jrParameters != null && jrParameters.length > 0) {
      this.parameters = new JRFillParameter[jrParameters.length];
      this.parametersMap = new HashMap();
      for (int i = 0; i < this.parameters.length; i++) {
        this.parameters[i] = factory.getParameter(jrParameters[i]);
        this.parametersMap.put(this.parameters[i].getName(), this.parameters[i]);
      } 
    } 
  }
  
  private void setGroups(JRDataset dataset, JRFillObjectFactory factory) {
    JRGroup[] jrGroups = dataset.getGroups();
    if (jrGroups != null && jrGroups.length > 0) {
      this.groups = new JRFillGroup[jrGroups.length];
      for (int i = 0; i < this.groups.length; i++)
        this.groups[i] = factory.getGroup(jrGroups[i]); 
    } 
  }
  
  private void setVariables(JRDataset dataset, JRFillObjectFactory factory) {
    JRVariable[] jrVariables = dataset.getVariables();
    if (jrVariables != null && jrVariables.length > 0) {
      List variableList = new ArrayList(jrVariables.length * 3);
      this.variablesMap = new HashMap();
      for (int i = 0; i < jrVariables.length; i++)
        addVariable(jrVariables[i], variableList, factory); 
      setVariables(variableList);
    } 
  }
  
  private JRFillVariable addVariable(JRVariable parentVariable, List variableList, JRFillObjectFactory factory) {
    JRVariable jRVariable1, varianceVar, countVar;
    JRFillVariable jRFillVariable1, fillVarianceVar, fillCountVar;
    JRVariable sumVar;
    JRFillVariable fillSumVar, variable = factory.getVariable(parentVariable);
    byte calculation = variable.getCalculation();
    switch (calculation) {
      case 3:
      case 7:
        jRVariable1 = createHelperVariable(parentVariable, "_COUNT", (byte)1);
        jRFillVariable1 = addVariable(jRVariable1, variableList, factory);
        variable.setHelperVariable(jRFillVariable1, (byte)0);
        sumVar = createHelperVariable(parentVariable, "_SUM", (byte)2);
        fillSumVar = addVariable(sumVar, variableList, factory);
        variable.setHelperVariable(fillSumVar, (byte)1);
        break;
      case 6:
        varianceVar = createHelperVariable(parentVariable, "_VARIANCE", (byte)7);
        fillVarianceVar = addVariable(varianceVar, variableList, factory);
        variable.setHelperVariable(fillVarianceVar, (byte)2);
        break;
      case 10:
        countVar = createDistinctCountHelperVariable(parentVariable);
        fillCountVar = addVariable(countVar, variableList, factory);
        variable.setHelperVariable(fillCountVar, (byte)0);
        break;
    } 
    variableList.add(variable);
    return variable;
  }
  
  private JRVariable createHelperVariable(JRVariable variable, String nameSuffix, byte calculation) {
    JRDesignVariable helper = new JRDesignVariable();
    helper.setName(variable.getName() + nameSuffix);
    helper.setValueClassName(variable.getValueClassName());
    helper.setIncrementerFactoryClassName(variable.getIncrementerFactoryClassName());
    helper.setResetType(variable.getResetType());
    helper.setResetGroup(variable.getResetGroup());
    helper.setIncrementType(variable.getIncrementType());
    helper.setIncrementGroup(variable.getIncrementGroup());
    helper.setCalculation(calculation);
    helper.setSystemDefined(true);
    helper.setExpression(variable.getExpression());
    return (JRVariable)helper;
  }
  
  private JRVariable createDistinctCountHelperVariable(JRVariable variable) {
    JRDesignVariable helper = new JRDesignVariable();
    helper.setName(variable.getName() + "_DISTINCT_COUNT");
    helper.setValueClassName(variable.getValueClassName());
    helper.setIncrementerFactoryClassName(JRDistinctCountIncrementerFactory.class.getName());
    helper.setResetType((byte)1);
    if (variable.getIncrementType() != 5)
      helper.setResetType(variable.getIncrementType()); 
    helper.setResetGroup(variable.getIncrementGroup());
    helper.setCalculation((byte)0);
    helper.setSystemDefined(true);
    helper.setExpression(variable.getExpression());
    return (JRVariable)helper;
  }
  
  private void setVariables(List variableList) {
    this.variables = new JRFillVariable[variableList.size()];
    this.variables = (JRFillVariable[])variableList.toArray((Object[])this.variables);
    for (int i = 0; i < this.variables.length; i++)
      this.variablesMap.put(this.variables[i].getName(), this.variables[i]); 
  }
  
  private void setFields(JRDataset dataset, JRFillObjectFactory factory) {
    JRField[] jrFields = dataset.getFields();
    if (jrFields != null && jrFields.length > 0) {
      this.fields = new JRFillField[jrFields.length];
      this.fieldsMap = new HashMap();
      for (int i = 0; i < this.fields.length; i++) {
        this.fields[i] = factory.getField(jrFields[i]);
        this.fieldsMap.put(this.fields[i].getName(), this.fields[i]);
      } 
    } 
  }
  
  protected void createCalculator(JasperReport jasperReport) throws JRException {
    setCalculator(createCalculator(jasperReport, this));
  }
  
  protected void setCalculator(JRCalculator calculator) {
    this.calculator = calculator;
  }
  
  protected static JRCalculator createCalculator(JasperReport jasperReport, JRDataset dataset) throws JRException {
    JREvaluator evaluator = JasperCompileManager.loadEvaluator(jasperReport, dataset);
    return new JRCalculator(evaluator);
  }
  
  protected void initCalculator() throws JRException {
    this.calculator.init(this);
  }
  
  protected void inheritFromMain() {
    if (this.resourceBundleBaseName == null && !this.isMain) {
      this.resourceBundleBaseName = this.filler.mainDataset.resourceBundleBaseName;
      this.whenResourceMissingType = this.filler.mainDataset.whenResourceMissingType;
    } 
  }
  
  protected JRAbstractScriptlet createScriptlet() throws JRException {
    JRDefaultScriptlet jRDefaultScriptlet;
    JRAbstractScriptlet tmpScriptlet = null;
    if (this.scriptletClassName != null) {
      try {
        Class scriptletClass = JRClassLoader.loadClassForName(this.scriptletClassName);
        tmpScriptlet = scriptletClass.newInstance();
      } catch (ClassNotFoundException e) {
        throw new JRException("Error loading scriptlet class : " + this.scriptletClassName, e);
      } catch (Exception e) {
        throw new JRException("Error creating scriptlet class instance : " + this.scriptletClassName, e);
      } 
    } else {
      jRDefaultScriptlet = new JRDefaultScriptlet();
    } 
    return (JRAbstractScriptlet)jRDefaultScriptlet;
  }
  
  protected void initElementDatasets(JRFillObjectFactory factory) {
    this.elementDatasets = factory.getElementDatasets(this);
  }
  
  protected void filterElementDatasets(JRFillElementDataset elementDataset) {
    this.origElementDatasets = this.elementDatasets;
    this.elementDatasets = new JRFillElementDataset[] { elementDataset };
  }
  
  protected void restoreElementDatasets() {
    if (this.origElementDatasets != null) {
      this.elementDatasets = this.origElementDatasets;
      this.origElementDatasets = null;
    } 
  }
  
  protected ResourceBundle loadResourceBundle() {
    ResourceBundle loadedBundle;
    if (this.resourceBundleBaseName == null) {
      loadedBundle = null;
    } else {
      loadedBundle = JRResourcesUtil.loadResourceBundle(this.resourceBundleBaseName, this.locale);
    } 
    return loadedBundle;
  }
  
  protected void setParameterValues(Map parameterValues) throws JRException {
    parameterValues.put("REPORT_PARAMETERS_MAP", parameterValues);
    this.reportMaxCount = (Integer)parameterValues.get("REPORT_MAX_COUNT");
    this.locale = (Locale)parameterValues.get("REPORT_LOCALE");
    if (this.locale == null) {
      this.locale = Locale.getDefault();
      parameterValues.put("REPORT_LOCALE", this.locale);
    } 
    this.resourceBundle = (ResourceBundle)parameterValues.get("REPORT_RESOURCE_BUNDLE");
    if (this.resourceBundle == null) {
      this.resourceBundle = loadResourceBundle();
      if (this.resourceBundle != null)
        parameterValues.put("REPORT_RESOURCE_BUNDLE", this.resourceBundle); 
    } 
    this.timeZone = (TimeZone)parameterValues.get("REPORT_TIME_ZONE");
    if (this.timeZone == null) {
      this.timeZone = TimeZone.getDefault();
      parameterValues.put("REPORT_TIME_ZONE", this.timeZone);
    } 
    this.scriptlet = (JRAbstractScriptlet)parameterValues.get("REPORT_SCRIPTLET");
    if (this.scriptlet == null) {
      this.scriptlet = createScriptlet();
      parameterValues.put("REPORT_SCRIPTLET", this.scriptlet);
    } 
    this.scriptlet.setData(this.parametersMap, this.fieldsMap, this.variablesMap, this.groups);
    setFillParameterValues(parameterValues);
  }
  
  protected void initDatasource() throws JRException {
    this.queryExecuter = null;
    this.dataSource = (JRDataSource)getParameterValue("REPORT_DATA_SOURCE");
    if (!this.useDatasourceParamValue && (this.useConnectionParamValue || this.dataSource == null)) {
      this.dataSource = createQueryDatasource();
      setParameter("REPORT_DATA_SOURCE", this.dataSource);
    } 
    JRSortField[] sortFields = getSortFields();
    if (sortFields != null && sortFields.length > 0) {
      this.dataSource = (JRDataSource)new JRSortableDataSource(this.dataSource, (JRField[])this.fields, sortFields, this.locale);
      setParameter("REPORT_DATA_SOURCE", this.dataSource);
    } 
  }
  
  private void setFillParameterValues(Map parameterValues) throws JRException {
    if (this.parameters != null && this.parameters.length > 0)
      for (int i = 0; i < this.parameters.length; i++) {
        Object value = null;
        if (parameterValues.containsKey(this.parameters[i].getName())) {
          value = parameterValues.get(this.parameters[i].getName());
        } else if (!this.parameters[i].isSystemDefined()) {
          value = this.calculator.evaluate(this.parameters[i].getDefaultValueExpression(), (byte)3);
          if (value != null)
            parameterValues.put(this.parameters[i].getName(), value); 
        } 
        setParameter(this.parameters[i], value);
      }  
  }
  
  private JRDataSource createQueryDatasource() throws JRException {
    if (this.query == null)
      return null; 
    try {
      if (log.isDebugEnabled())
        log.debug("Fill " + this.filler.fillerId + ": Creating " + this.query.getLanguage() + " query executer"); 
      JRQueryExecuterFactory queryExecuterFactory = JRQueryExecuterUtils.getQueryExecuterFactory(this.query.getLanguage());
      this.queryExecuter = queryExecuterFactory.createQueryExecuter(this.parent, this.parametersMap);
      this.filler.fillContext.setRunningQueryExecuter(this.queryExecuter);
      return this.queryExecuter.createDatasource();
    } finally {
      this.filler.fillContext.clearRunningQueryExecuter();
    } 
  }
  
  protected void reset() {
    this.useDatasourceParamValue = false;
    this.useConnectionParamValue = false;
  }
  
  protected void setDatasourceParameterValue(Map parameterValues, JRDataSource ds) {
    this.useDatasourceParamValue = true;
    if (ds != null)
      parameterValues.put("REPORT_DATA_SOURCE", ds); 
  }
  
  protected void setConnectionParameterValue(Map parameterValues, Connection conn) {
    this.useConnectionParamValue = true;
    if (conn != null)
      parameterValues.put("REPORT_CONNECTION", conn); 
  }
  
  protected void closeDatasource() {
    if (this.queryExecuter != null) {
      if (log.isDebugEnabled())
        log.debug("Fill " + this.filler.fillerId + ": closing query executer"); 
      this.queryExecuter.close();
      this.queryExecuter = null;
    } 
    reset();
  }
  
  protected void start() {
    this.reportCount = 0;
  }
  
  protected boolean next() throws JRException {
    boolean hasNext = false;
    if (this.dataSource != null) {
      boolean includeRow = true;
      JRExpression filterExpression = getFilterExpression();
      do {
        hasNext = advanceDataSource();
        if (!hasNext)
          continue; 
        setOldValues();
        this.calculator.estimateVariables();
        if (filterExpression != null) {
          Boolean filterExprResult = (Boolean)this.calculator.evaluate(filterExpression, (byte)2);
          includeRow = (filterExprResult != null && filterExprResult.booleanValue());
        } 
        if (includeRow)
          continue; 
        revertToOldValues();
      } while (hasNext && !includeRow);
      if (hasNext)
        this.reportCount++; 
    } 
    return hasNext;
  }
  
  protected void setOldValues() throws JRException {
    if (this.fields != null && this.fields.length > 0)
      for (int i = 0; i < this.fields.length; i++) {
        JRFillField field = this.fields[i];
        field.setPreviousOldValue(field.getOldValue());
        field.setOldValue(field.getValue());
        field.setValue(this.dataSource.getFieldValue(field));
      }  
    if (this.variables != null && this.variables.length > 0)
      for (int i = 0; i < this.variables.length; i++) {
        JRFillVariable variable = this.variables[i];
        variable.setPreviousOldValue(variable.getOldValue());
        variable.setOldValue(variable.getValue());
      }  
  }
  
  protected void revertToOldValues() {
    if (this.fields != null && this.fields.length > 0)
      for (int i = 0; i < this.fields.length; i++) {
        JRFillField field = this.fields[i];
        field.setValue(field.getOldValue());
        field.setOldValue(field.getPreviousOldValue());
      }  
    if (this.variables != null && this.variables.length > 0)
      for (int i = 0; i < this.variables.length; i++) {
        JRFillVariable variable = this.variables[i];
        variable.setValue(variable.getOldValue());
        variable.setOldValue(variable.getPreviousOldValue());
      }  
  }
  
  protected boolean advanceDataSource() throws JRException {
    boolean hasNext = ((this.reportMaxCount == null || this.reportMaxCount.intValue() > this.reportCount) && this.dataSource.next());
    return hasNext;
  }
  
  protected void setParameter(String parameterName, Object value) throws JRException {
    JRFillParameter parameter = (JRFillParameter)this.parametersMap.get(parameterName);
    if (parameter != null)
      setParameter(parameter, value); 
  }
  
  protected void setParameter(JRFillParameter parameter, Object value) throws JRException {
    if (value != null) {
      if (parameter.getValueClass().isInstance(value)) {
        parameter.setValue(value);
      } else {
        throw new JRException("Incompatible " + value.getClass().getName() + " value assigned to parameter " + parameter.getName() + " in the " + getName() + " dataset.");
      } 
    } else {
      parameter.setValue(value);
    } 
  }
  
  public Object getVariableValue(String variableName) {
    JRFillVariable var = (JRFillVariable)this.variablesMap.get(variableName);
    if (var == null)
      throw new JRRuntimeException("No such variable " + variableName); 
    return var.getValue();
  }
  
  public Object getParameterValue(String parameterName) {
    return getParameterValue(parameterName, false);
  }
  
  public Object getParameterValue(String parameterName, boolean ignoreMissing) {
    Object value;
    JRFillParameter param = (JRFillParameter)this.parametersMap.get(parameterName);
    if (param == null) {
      if (!ignoreMissing)
        throw new JRRuntimeException("No such parameter " + parameterName); 
      value = null;
    } else {
      value = param.getValue();
    } 
    return value;
  }
  
  public Object getFieldValue(String fieldName) {
    JRFillField var = (JRFillField)this.fieldsMap.get(fieldName);
    if (var == null)
      throw new JRRuntimeException("No such field " + fieldName); 
    return var.getValue();
  }
  
  protected static class VariableCalculationReq {
    String variableName;
    
    byte calculation;
    
    VariableCalculationReq(String variableName, byte calculation) {
      this.variableName = variableName;
      this.calculation = calculation;
    }
    
    public boolean equals(Object o) {
      if (o == null || !(o instanceof VariableCalculationReq))
        return false; 
      VariableCalculationReq r = (VariableCalculationReq)o;
      return (this.variableName.equals(r.variableName) && this.calculation == r.calculation);
    }
    
    public int hashCode() {
      return 31 * this.calculation + this.variableName.hashCode();
    }
  }
  
  protected void addVariableCalculationReq(String variableName, byte calculation) {
    if (this.variableCalculationReqs == null)
      this.variableCalculationReqs = new HashSet(); 
    this.variableCalculationReqs.add(new VariableCalculationReq(variableName, calculation));
  }
  
  protected void checkVariableCalculationReqs(JRFillObjectFactory factory) {
    if (this.variableCalculationReqs != null && !this.variableCalculationReqs.isEmpty()) {
      List variableList = new ArrayList(this.variables.length * 2);
      for (int i = 0; i < this.variables.length; i++) {
        JRFillVariable variable = this.variables[i];
        checkVariableCalculationReq(variable, variableList, factory);
      } 
      setVariables(variableList);
    } 
  }
  
  private void checkVariableCalculationReq(JRFillVariable variable, List variableList, JRFillObjectFactory factory) {
    if (hasVariableCalculationReq(variable, (byte)3) || hasVariableCalculationReq(variable, (byte)7)) {
      if (variable.getHelperVariable((byte)0) == null) {
        JRVariable countVar = createHelperVariable(variable, "_COUNT", (byte)1);
        JRFillVariable fillCountVar = factory.getVariable(countVar);
        checkVariableCalculationReq(fillCountVar, variableList, factory);
        variable.setHelperVariable(fillCountVar, (byte)0);
      } 
      if (variable.getHelperVariable((byte)1) == null) {
        JRVariable sumVar = createHelperVariable(variable, "_SUM", (byte)2);
        JRFillVariable fillSumVar = factory.getVariable(sumVar);
        checkVariableCalculationReq(fillSumVar, variableList, factory);
        variable.setHelperVariable(fillSumVar, (byte)1);
      } 
    } 
    if (hasVariableCalculationReq(variable, (byte)6))
      if (variable.getHelperVariable((byte)2) == null) {
        JRVariable varianceVar = createHelperVariable(variable, "_VARIANCE", (byte)7);
        JRFillVariable fillVarianceVar = factory.getVariable(varianceVar);
        checkVariableCalculationReq(fillVarianceVar, variableList, factory);
        variable.setHelperVariable(fillVarianceVar, (byte)2);
      }  
    if (hasVariableCalculationReq(variable, (byte)10))
      if (variable.getHelperVariable((byte)0) == null) {
        JRVariable countVar = createDistinctCountHelperVariable(variable);
        JRFillVariable fillCountVar = factory.getVariable(countVar);
        checkVariableCalculationReq(fillCountVar, variableList, factory);
        variable.setHelperVariable(fillCountVar, (byte)0);
      }  
    variableList.add(variable);
  }
  
  private boolean hasVariableCalculationReq(JRVariable var, byte calculation) {
    return this.variableCalculationReqs.contains(new VariableCalculationReq(var.getName(), calculation));
  }
  
  public String getName() {
    return this.parent.getName();
  }
  
  public String getScriptletClass() {
    return this.parent.getScriptletClass();
  }
  
  public JRParameter[] getParameters() {
    return (JRParameter[])this.parameters;
  }
  
  public Map getParametersMap() {
    return this.parametersMap;
  }
  
  public JRQuery getQuery() {
    return this.query;
  }
  
  public JRField[] getFields() {
    return (JRField[])this.fields;
  }
  
  public JRSortField[] getSortFields() {
    return this.parent.getSortFields();
  }
  
  public JRVariable[] getVariables() {
    return (JRVariable[])this.variables;
  }
  
  public JRGroup[] getGroups() {
    return (JRGroup[])this.groups;
  }
  
  public boolean isMainDataset() {
    return this.isMain;
  }
  
  public String getResourceBundle() {
    return this.parent.getResourceBundle();
  }
  
  public byte getWhenResourceMissingType() {
    return this.whenResourceMissingType;
  }
  
  public void setWhenResourceMissingType(byte whenResourceMissingType) {
    this.whenResourceMissingType = whenResourceMissingType;
  }
  
  public boolean hasProperties() {
    return this.parent.hasProperties();
  }
  
  public JRPropertiesMap getPropertiesMap() {
    return this.parent.getPropertiesMap();
  }
  
  public JRPropertiesHolder getParentProperties() {
    return null;
  }
  
  public JRExpression getFilterExpression() {
    return this.parent.getFilterExpression();
  }
  
  public Object clone() {
    return null;
  }
}
