/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.amber.query;

import com.caucho.amber.entity.AmberEntityHome;
import com.caucho.amber.expr.AmberExpr;
import com.caucho.amber.expr.AndExpr;
import com.caucho.amber.expr.ArgExpr;
import com.caucho.amber.expr.EmbeddedExpr;
import com.caucho.amber.expr.JoinExpr;
import com.caucho.amber.expr.ManyToOneJoinExpr;
import com.caucho.amber.manager.AmberConnection;
import com.caucho.amber.table.LinkColumns;
import com.caucho.amber.table.Table;
import com.caucho.amber.type.EntityType;
import com.caucho.jdbc.JdbcMetaData;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * Represents an amber query
 */
abstract public class AbstractQuery {
  private String _sql;

  AmberExpr _where;
  AmberExpr _having;

  protected ArrayList<FromItem> _fromList = new ArrayList<FromItem>();

  // jpa/0w22
  // SELECT p.startMonth FROM TestBean o JOIN o.period p
  // p is an alias to o.period (o.period is @Embedded)
  // "p" -> "o.period"
  protected HashMap<String, EmbeddedExpr> _embeddedAliases
    = new HashMap<String, EmbeddedExpr>();

  private ArgExpr []_argList;

  // Map named parameters to JDBC ?,?,?.
  // Ex: INSERT INTO test VALUES(:testId, :testName) is mapped as [0]->"testId", [1]->"testName"
  //     INSERT INTO test VALUES(:testName, :testName) is mapped as [0]->"testName", [1]->"testName"
  // XXX: HashMap<String, ArrayList<Long>> would probably be an overkill.
  //
  private ArrayList<String> _preparedMapping = new ArrayList<String>();

  private JdbcMetaData _metaData;

  // jpa/1231
  private boolean _hasSubQuery;


  AbstractQuery(String sql, JdbcMetaData metaData)
  {
    _sql = sql;
    _metaData = metaData;
  }

  /**
   * Returns the query string.
   */
  public String getQueryString()
  {
    return _sql;
  }

  /**
   * Adds an embedded alias.
   */
  public void addEmbeddedAlias(String alias,
                               EmbeddedExpr expr)
  {
    _embeddedAliases.put(alias, expr);
  }

  /**
   * Gets the embedded aliases.
   */
  public HashMap<String, EmbeddedExpr> getEmbeddedAliases()
  {
    return _embeddedAliases;
  }

  /**
   * Sets the from list.
   */
  public FromItem createFromItem(Table table,
                                 String name)
  {
    return createFromItem(null, table, name);
  }

  /**
   * Sets the from list.
   */
  public FromItem createFromItem(EntityType entityType,
                                 Table table,
                                 String name)
  {
    FromItem item = new FromItem(entityType, table,
                                 name, _fromList.size());

    item.setQuery(this);

    _fromList.add(item);

    return item;
  }

  /**
   * Creates a dependent from item
   */
  public FromItem createDependentFromItem(FromItem parent,
                                          LinkColumns link,
                                          String name)
  {
    for (int i = 0; i < _fromList.size(); i++) {
      JoinExpr join = _fromList.get(i).getJoinExpr();

      if (join != null && join.isDependent(parent, link))
        return _fromList.get(i);
    }

    FromItem item = createFromItem(null, link.getSourceTable(), name);

    JoinExpr join = new ManyToOneJoinExpr(link, item, parent);

    item.setJoinExpr(join);

    return item;
  }

  /**
   * Returns the from list.
   */
  public ArrayList<FromItem> getFromList()
  {
    return _fromList;
  }

  /**
   * Gets the parent query.
   */
  public AbstractQuery getParentQuery()
  {
    return null;
  }

  /**
   * Returns the prepared mapping.
   */
  public ArrayList<String> getPreparedMapping()
  {
    return _preparedMapping;
  }

  /**
   * Returns the SQL.
   */
  public abstract String getSQL();

  /**
   * initializes the query.
   */
  void init()
    throws QueryParseException
  {
    if (_where instanceof AndExpr) {
      AndExpr and = (AndExpr) _where;

      ArrayList<AmberExpr> components = and.getComponents();

      for (int i = components.size() - 1; i >= 0; i--) {
        AmberExpr component = components.get(i);

        if (component instanceof JoinExpr) {
          JoinExpr link = (JoinExpr) component;

          if (link.bindToFromItem()) {
            components.remove(i);
          }
        }
      }

      _where = and.getSingle();
    }

    if (_having instanceof AndExpr) {
      AndExpr and = (AndExpr) _having;

      ArrayList<AmberExpr> components = and.getComponents();

      for (int i = components.size() - 1; i >= 0; i--) {
        AmberExpr component = components.get(i);

        if (component instanceof JoinExpr) {
          JoinExpr link = (JoinExpr) component;

          if (link.bindToFromItem()) {
            components.remove(i);
          }
        }
      }

      _having = and.getSingle();
    }

    // Rolls up unused from items from the left to the right.
    // It's not necessary to roll up the rightmost items because
    // they're only created if they're actually needed
    for (int i = 0; i < _fromList.size(); i++) {
      FromItem item = _fromList.get(i);

      JoinExpr join = item.getJoinExpr();

      if (join == null)
        continue;

      // XXX: jpa/1173, jpa/1178
      // if (getParentQuery() != null)
      //   break;

      FromItem joinParent = join.getJoinParent();
      FromItem joinTarget = join.getJoinTarget();

      boolean isTarget = item == joinTarget;

      if (joinParent == null) {
      }
      else if (joinParent.getJoinExpr() == null
               && joinParent == joinTarget
               && ! usesFromData(joinParent)) {
        _fromList.remove(joinParent);

        replaceJoin(join);

        // XXX:
        item.setJoinExpr(null);
        //item.setOuterJoin(false);
        i = -1;

        AmberExpr joinWhere = join.getWhere();

        if (joinWhere != null)
          _where = AndExpr.create(_where, joinWhere);
      }
      else if (item == joinTarget
               && ! isJoinParent(item)
               && ! usesFromData(item)) {

        boolean isManyToOne = false;
        boolean isManyToMany = false;

        if (join instanceof ManyToOneJoinExpr) {
          // jpa/0h1c
          isManyToOne = true;

          // jpa/1144
          ManyToOneJoinExpr manyToOneJoinExpr;
          manyToOneJoinExpr = (ManyToOneJoinExpr) join;
          isManyToMany = manyToOneJoinExpr.isManyToMany();
        }

        // ejb/06u0, jpa/1144, jpa/0h1c, jpa/114g
        if (isManyToMany || (isManyToOne && ! item.isInnerJoin())) {
          // ejb/06u0 || isFromInnerJoin(item)))) {

          // Optimization for common children query:
          // SELECT o FROM TestBean o WHERE o.parent.id=?
          // jpa/0h1k
          // jpa/114g as negative exists test

          // jpa/0h1m
          if (i + 1 < _fromList.size()) {
            FromItem subItem = _fromList.get(i + 1);

            JoinExpr nextJoin = subItem.getJoinExpr();

            if (nextJoin != null
                && nextJoin instanceof ManyToOneJoinExpr) {
              continue;
            }
          }

          _fromList.remove(item);

          replaceJoin(join);

          i = -1;

          AmberExpr joinWhere = join.getWhere();

          if (joinWhere != null)
            _where = AndExpr.create(_where, joinWhere);
        }
      }
    }

    for (int i = 0; i < _fromList.size(); i++) {
      FromItem item = _fromList.get(i);

      if (item.isInnerJoin())
        continue;

      if (item.getJoinExpr() == null)
        continue;

      boolean isFromInner = isFromInnerJoin(item);

      item.setOuterJoin(! isFromInner);
    }
  }

  boolean isJoinParent(FromItem item)
  {
    for (int i = 0; i < _fromList.size(); i++) {
      FromItem subItem = _fromList.get(i);

      if (subItem.getJoinExpr() != null &&
          subItem.getJoinExpr().getJoinParent() == item) {
        return true;
      }
    }

    return false;
  }

  boolean isFromInnerJoin(FromItem item)
  {
    return usesFrom(item, AmberExpr.IS_INNER_JOIN);
  }

  boolean usesFromData(FromItem item)
  {
    return usesFrom(item, AmberExpr.USES_DATA);
  }

  /**
   * Returns true if this query has a subquery.
   */
  public boolean hasSubQuery()
  {
    return _hasSubQuery;
  }

  /**
   * Sets true if this query has a subquery.
   */
  public void setHasSubQuery(boolean hasSubQuery)
  {
    _hasSubQuery = hasSubQuery;
  }

  /**
   * Returns true if the item must have at least one entry in the database.
   */
  public boolean exists(FromItem item)
  {
    if (_where != null && _where.exists(item)) {
      return true;
    }

    return false;
  }

  /**
   * Returns true if the from item is used by the query.
   */
  public boolean usesFrom(FromItem item, int type)
  {
    // jpa/1201
    if (_where != null && _where.usesFrom(item, type)) {
      return true;
    }

    return false;
  }

  void replaceJoin(JoinExpr join)
  {
    if (_where != null) {
      _where = _where.replaceJoin(join);
    }
  }

  /**
   * Sets the arg list.
   */
  boolean setArgList(ArgExpr []argList)
  {
    _argList = argList;

    int n = argList.length;

    if (n > 0) {

      if (argList[0].getName() != null) {

        for (int i=0; i < n; i++) {

          String name = argList[i].getName();

          if (name == null) {
            _preparedMapping = null;
            return false;
          }

          _preparedMapping.add(name);
        }
      }
    }

    return true;
  }

  /**
   * Returns the arg list.
   */
  public ArgExpr []getArgList()
  {
    return _argList;
  }

  /**
   * Generates update
   */
  void registerUpdates(CachedQuery query)
  {
    for (int i = 0; i < _fromList.size(); i++) {
      FromItem item = _fromList.get(i);

      AmberEntityHome home = item.getEntityHome();

      CacheUpdate update = new TableCacheUpdate(query);

      home.addUpdate(update);
    }
  }

  /**
   * Returns the expire time.
   */
  public long getCacheMaxAge()
  {
    return -1;
  }

  /**
   * Prepares before any update.
   */
  public void prepare(UserQuery userQuery, AmberConnection aConn)
    throws SQLException
  {
  }

  /**
   * Any post-sql completion
   */
  public void complete(UserQuery userQuery, AmberConnection aConn)
    throws SQLException
  {
  }

  /**
   * Returns the jdbc meta data, if available.
   */
  JdbcMetaData getMetaData()
  {
    return _metaData;
  }
}
