/**
 * Copyright (c) 2010 Kenn Hussey and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors: 
 *   Kenn Hussey - Initial API and implementation
 */
package org.eclipse.emf.oda.ecore.impl;

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.eclipse.datatools.connectivity.oda.IParameterMetaData;
import org.eclipse.datatools.connectivity.oda.IQuery;
import org.eclipse.datatools.connectivity.oda.IResultSet;
import org.eclipse.datatools.connectivity.oda.IResultSetMetaData;
import org.eclipse.datatools.connectivity.oda.OdaException;
import org.eclipse.datatools.connectivity.oda.SortSpec;
import org.eclipse.datatools.connectivity.oda.spec.QuerySpecification;
import org.eclipse.datatools.connectivity.oda.spec.QuerySpecification.ParameterIdentifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.TreeIterator;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.common.util.UniqueEList;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EClassifier;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.emf.ecore.util.QueryDelegate;
import org.eclipse.emf.oda.ecore.util.StringUtil;


/**
 * Implementation of IQuery for EMF ODA runtime driver.
 */
public class Query implements IQuery
{
  public static final String DELEGATE_PROPERTY_NAME = "delegate"; //$NON-NLS-1$
  public static final String CONTEXT_PROPERTY_NAME = "context"; //$NON-NLS-1$
  public static final String VARIABLES_PROPERTY_NAME = "variables"; //$NON-NLS-1$
  public static final String TYPE_PROPERTY_NAME = "type"; //$NON-NLS-1$

  protected final Connection connection;

  protected QuerySpecification specification = null;

  protected QueryDelegate.Factory factory = null;
  protected QueryDelegate delegate = null;
  protected EClassifier context = null;
  protected Map<String, EClassifier> variables = null;
  protected EClassifier type = null;

  protected String queryText = null;
  protected int maxRows = 0;

  protected Query(Connection connection)
  {
    super();

    this.connection = connection;
  }

  /**
   * Asserts that a specification has been provided for this query.
   * @throws OdaException if a specification has not been provided
   */
  protected void assertSpecified() throws OdaException
  {
    if (specification == null)
    {
      throw new OdaException(new IllegalStateException());
    }
  }

  /**
   * Asserts that this query has been prepared.
   * @throws OdaException if the query has not been prepared
   */
  protected void assertPrepared() throws OdaException
  {
    if (delegate == null || context == null || type == null)
    {
      throw new OdaException(new IllegalStateException());
    }
  }

  /**
   * Returns the expression variables specified for this query.
   * @return a map of variable names to types
   * @throws OdaException if a specification has not been provided
   */
  protected Map<String, EClassifier> getVariables() throws OdaException
  {
    assertSpecified();

    if (variables == null)
    {
      variables = new HashMap<String, EClassifier>();

      String string = (String)specification.getProperty(VARIABLES_PROPERTY_NAME);

      if (!StringUtil.isEmpty(string))
      {
        ResourceSet resourceSet = connection.getResourceSet();
        String[] strings = string.split(Pattern.quote(" ")); //$NON-NLS-1$

        for (int i = 0; i < strings.length; i += 2)
        {
          variables.put(strings[i], (EClassifier)resourceSet.getEObject(URI.createURI((strings[i + 1])), true));
        }
      }
    }

    return variables;
  }

  public void prepare(String queryText) throws OdaException
  {
    assertSpecified();

    if (StringUtil.isEmpty(queryText))
    {
      throw new OdaException(new IllegalArgumentException(queryText));
    }

    this.queryText = queryText;

    try
    {
      factory = (QueryDelegate.Factory)QueryDelegate.Factory.Registry.INSTANCE.get(specification.getProperty(DELEGATE_PROPERTY_NAME));

      ResourceSet resourceSet = connection.getResourceSet();

      context = (EClassifier)resourceSet.getEObject(URI.createURI(((String)specification.getProperty(CONTEXT_PROPERTY_NAME))), true);

      Map<String, EClassifier> variables = getVariables();
      delegate = factory.createQueryDelegate(context, variables.isEmpty() ? null : variables, queryText);

      type = (EClassifier)resourceSet.getEObject(URI.createURI(((String)specification.getProperty(TYPE_PROPERTY_NAME))), true);

      delegate.prepare();
    }
    catch (Exception e)
    {
      throw new OdaException(e);
    }
  }

  public void setAppContext(Object context) throws OdaException
  {
    // do nothing; no support for pass-through context
  }

  public void close() throws OdaException
  {
    delegate = null;
    context = null;
    type = null;
  }

  public IResultSetMetaData getMetaData() throws OdaException
  {
    assertPrepared();
    return ResultSetMetaData.create(type);
  }

  protected EList<Object> getAllObjectsByType(EList<Object> objects, EClassifier type)
  {
    for (TreeIterator<Object> allContents = EcoreUtil.getAllContents(connection.getResourceSet(), true); allContents.hasNext();)
    {
      Object next = allContents.next();

      if (type.isInstance(next))
      {
        objects.add(next);
      }
    }

    return objects;
  }

  protected EList<Object> getResults(
    EList<Object> results,
    QueryDelegate delegate,
    EList<Object> targets,
    Map<String, Object> arguments,
    EClassifier type) throws InvocationTargetException
  {
    for (Object target : targets)
    {
      Object result = delegate.execute(target, arguments);

      if (result instanceof Collection< ? >)
      {
        results.addAll(EcoreUtil.getObjectsByType((Collection< ? >)result, type));
      }
      else if (type.isInstance(result))
      {
        results.add(result);
      }
    }

    return results;
  }

  public IResultSet executeQuery() throws OdaException
  {
    assertPrepared();

    EList<Object> targets = new UniqueEList.FastCompare<Object>();
    Map<String, Object> arguments = new HashMap<String, Object>();

    Map<ParameterIdentifier, ? > parameterValues = specification.getParameterValues();

    if (!parameterValues.isEmpty())
    {
      Object targetArgument = null;

      Map<String, EClassifier> dataTypeParameters = new HashMap<String, EClassifier>();
      Map<String, Object> dataTypeArguments = new HashMap<String, Object>();

      Map<String, Object> classArguments = new HashMap<String, Object>();

      for (Map.Entry<ParameterIdentifier, ? > entry : parameterValues.entrySet())
      {
        String name = entry.getKey().getParameterName();

        if (!StringUtil.isEmpty(name))
        {
          Object value = entry.getValue();

          if (value instanceof ResultSet.JavaObject)
          {
            value = ((ResultSet.JavaObject)value).getObject();
          }

          if (value != null)
          {
            if (ParameterMetaData.TARGET_PARAMETER_NAME.equals(name))
            {
              targetArgument = value;
            }
            else
            {
              EClassifier type = variables.get(name);

              if (type instanceof EClass)
              {
                classArguments.put(name, value);
              }
              else
              {
                dataTypeParameters.put(name, type);
                dataTypeArguments.put(name, value);
              }
            }
          }
        }
      }

      if (targetArgument == null || ParameterMetaData.DEFAULT_PARAMETER_VALUE.equals(targetArgument))
      {
        getAllObjectsByType(targets, context);
      }
      else if (targetArgument instanceof String)
      {
        try
        {
          QueryDelegate delegate = factory.createQueryDelegate(context, dataTypeParameters, (String)targetArgument);
          getResults(targets, delegate, getAllObjectsByType(new UniqueEList.FastCompare<Object>(), context), dataTypeArguments, context);
        }
        catch (Exception e)
        {
          Throwable cause = e.getCause();
          throw new OdaException(ParameterMetaData.TARGET_PARAMETER_NAME
            + ": " + (cause == null ? e.getLocalizedMessage() : cause.getLocalizedMessage())); //$NON-NLS-1$
        }
      }
      else if (targetArgument instanceof Collection< ? >)
      {
        targets.addAll((Collection< ? >)targetArgument);
      }
      else
      {
        targets.add(targetArgument);
      }

      arguments.putAll(dataTypeArguments);

      for (Map.Entry<String, Object> entry : classArguments.entrySet())
      {
        String name = entry.getKey();
        Object value = entry.getValue();

        if (value instanceof String)
        {
          EClassifier type = variables.get(name);
          EList<Object> values = getAllObjectsByType(new UniqueEList.FastCompare<Object>(), type);

          if (!ParameterMetaData.DEFAULT_PARAMETER_VALUE.equals(value))
          {
            try
            {
              QueryDelegate delegate = factory.createQueryDelegate(type, dataTypeParameters, (String)value);
              values = getResults(new UniqueEList.FastCompare<Object>(), delegate, values, dataTypeArguments, type);
            }
            catch (Exception e)
            {
              Throwable cause = e.getCause();
              throw new OdaException(name + ": " + (cause == null ? e.getLocalizedMessage() : cause.getLocalizedMessage())); //$NON-NLS-1$
            }
          }

          value = values.isEmpty() ? null : values.iterator().next();
        }

        arguments.put(name, value);
      }
    }

    try
    {
      IResultSet resultSet = ResultSet.create(type, getResults(new UniqueEList<Object>(), delegate, targets, arguments, type));
      resultSet.setMaxRows(getMaxRows());
      return resultSet;
    }
    catch (Exception e)
    {
      Throwable cause = e.getCause();

      throw new OdaException(cause == null ? e.getLocalizedMessage() : cause.getLocalizedMessage());
    }
  }

  public void setProperty(String name, String value) throws OdaException
  {
    assertSpecified();
    specification.setProperty(name, value);
  }

  public void setMaxRows(int max) throws OdaException
  {
    maxRows = max;
  }

  public int getMaxRows() throws OdaException
  {
    return maxRows;
  }

  public void clearInParameters() throws OdaException
  {
    throw new OdaException(new UnsupportedOperationException());
  }

  public void setInt(String parameterName, int value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setInt(int parameterId, int value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setDouble(String parameterName, double value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setDouble(int parameterId, double value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setBigDecimal(String parameterName, BigDecimal value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setBigDecimal(int parameterId, BigDecimal value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setString(String parameterName, String value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setString(int parameterId, String value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setDate(String parameterName, Date value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setDate(int parameterId, Date value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setTime(String parameterName, Time value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setTime(int parameterId, Time value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setTimestamp(String parameterName, Timestamp value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setTimestamp(int parameterId, Timestamp value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setBoolean(String parameterName, boolean value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setBoolean(int parameterId, boolean value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setObject(String parameterName, Object value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, value);
  }

  public void setObject(int parameterId, Object value) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, value);
  }

  public void setNull(String parameterName) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterName, null);
  }

  public void setNull(int parameterId) throws OdaException
  {
    assertSpecified();
    specification.setParameterValue(parameterId, null);
  }

  public int findInParameter(String parameterName) throws OdaException
  {
    assertSpecified();

    for (Map.Entry<ParameterIdentifier, ? > entry : specification.getParameterValues().entrySet())
    {
      ParameterIdentifier identifier = entry.getKey();

      if (identifier.hasName() && identifier.getParameterName().equals(parameterName))
      {
        return identifier.getParameterId();
      }
    }

    throw new OdaException(new IllegalArgumentException(parameterName));
  }

  public IParameterMetaData getParameterMetaData() throws OdaException
  {
    assertSpecified();
    return new ParameterMetaData(this);
  }

  public void setSortSpec(SortSpec sortBy) throws OdaException
  {
    // sorting not supported
    throw new OdaException(new UnsupportedOperationException());
  }

  public SortSpec getSortSpec() throws OdaException
  {
    // only applies to sorting
    return null;
  }

  public void setSpecification(QuerySpecification querySpec) throws OdaException, UnsupportedOperationException
  {
    specification = querySpec;
  }

  public QuerySpecification getSpecification()
  {
    return specification;
  }

  public String getEffectiveQueryText()
  {
    return queryText;
  }

  public void cancel() throws OdaException, UnsupportedOperationException
  {
    // unable to cancel while executing a query
    throw new UnsupportedOperationException();
  }
}