/*
 * Copyright (c) 1998-2012 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.db.sql;

import java.io.InputStream;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.caucho.db.block.Block;
import com.caucho.db.jdbc.GeneratedKeysResultSet;
import com.caucho.db.table.TableIterator;
import com.caucho.db.table.Column.ColumnType;
import com.caucho.db.xa.DbTransaction;
import com.caucho.inject.Module;
import com.caucho.util.FreeList;
import com.caucho.util.L10N;

/**
 * Represents the state of the query at any particular time.
 */
@Module
public class QueryContext {
  private static final Logger log
    = Logger.getLogger(QueryContext.class.getName());
  private static final L10N L = new L10N(QueryContext.class);

  private static final long LOCK_TIMEOUT = 120000;

  private static final FreeList<QueryContext> _freeList
    = new FreeList<QueryContext>(64);

  private DbTransaction _xa;
  private TableIterator []_tableIterators;
  private boolean _isWrite;

  private Data []_parameters = new Data[16];

  private GroupItem _tempGroupItem;
  private GroupItem _groupItem;

  private boolean _isReturnGeneratedKeys;
  private SelectResult _result;
  private GeneratedKeysResultSet _generatedKeys;
  private int _rowUpdateCount;

  private int _limit = -1;

  private Block []_blockLocks;
  private int _blockLockLength;

  private boolean _isLocked;
  private boolean _isNonLocking;

  private HashMap<GroupItem,GroupItem> _groupMap;

  private byte []_buffer = new byte[256];

  private Thread _thread;

  private QueryContext()
  {
    _tempGroupItem = GroupItem.allocate(new boolean[8]);
  }

  /**
   * Returns a new query context.
   */
  public static QueryContext allocate()
  {
    QueryContext queryContext = _freeList.allocate();

    if (queryContext == null)
      queryContext = new QueryContext();

    queryContext.clearParameters();
    queryContext._limit = -1;
    queryContext._isNonLocking = false;

    return queryContext;
  }
  
  public void setNonLocking()
  {
    _isNonLocking = true;
  }

  public void clearParameters()
  {
    for (int i = _parameters.length - 1; i >= 0; i--) {
      if (_parameters[i] == null)
        _parameters[i] = new Data();

      _parameters[i].clear();
    }
  }

  /**
   * Initializes the query state.
   */
  public void init(DbTransaction xa,
                   TableIterator []tableIterators,
                   boolean isReadOnly)
  {
    if (_isLocked)
      throw new IllegalStateException();

    Thread thread = Thread.currentThread();

    if (_thread != null && _thread != thread)
      throw new IllegalStateException(toString() + " attempted query reuse without close");

    _thread = thread;

    _xa = xa;
    _isWrite = ! isReadOnly;
    _tableIterators = tableIterators;

    _blockLockLength = tableIterators.length;

    if (_blockLocks == null || _blockLocks.length < _blockLockLength)
      _blockLocks = new Block[_blockLockLength];
    else {
      for (int i = _blockLockLength - 1; i >= 0; i--)
        _blockLocks[i] = null;
    }

    _rowUpdateCount = 0;
    _groupItem = _tempGroupItem;
    _groupItem.init(0, null);
  }

  /**
   * Initializes the group.
   */
  public void initGroup(int size, boolean []isGroupByFields)
  {
    _groupItem = _tempGroupItem;

    _groupItem.init(size, isGroupByFields);

    if (_groupMap == null)
      _groupMap = new HashMap<GroupItem,GroupItem>();
  }

  /**
   * Selects the actual group item.
   */
  public void selectGroup()
  {
    GroupItem item = _groupMap.get(_groupItem);

    if (item == null) {
      item = _groupItem.allocateCopy();

      _groupMap.put(item, item);
    }

    _groupItem = item;
  }

  /**
   * Returns the group results.
   */
  Iterator<GroupItem> groupResults()
  {
    if (_groupMap == null)
      return com.caucho.util.NullIterator.create();

    Iterator<GroupItem> results = _groupMap.values().iterator();
    _groupMap = null;

    return results;
  }

  /**
   * Sets the current result.
   */
  void setGroupItem(GroupItem item)
  {
    _groupItem = item;
  }


  /**
   * Returns the table iterator.
   */
  public TableIterator []getTableIterators()
  {
    return _tableIterators;
  }

  /**
   * Sets the transaction.
   */
  public void setTransaction(DbTransaction xa)
  {
    _xa = xa;
  }

  /**
   * Returns the transaction.
   */
  public DbTransaction getTransaction()
  {
    return _xa;
  }

  /**
   * Returns the temp buffer.
   */
  public byte []getBuffer()
  {
    return _buffer;
  }

  /**
   * Returns the number of rows updated.
   */
  public int getRowUpdateCount()
  {
    return _rowUpdateCount;
  }

  /**
   * Sets the number of rows updated.
   */
  public void setRowUpdateCount(int count)
  {
    _rowUpdateCount = count;
  }

  /**
   * Set if the query should return the generated keys.
   */
  public boolean isReturnGeneratedKeys()
  {
    return _isReturnGeneratedKeys;
  }

  /**
   * Set if the query should return the generated keys.
   */
  public void setReturnGeneratedKeys(boolean isReturnGeneratedKeys)
  {
    _isReturnGeneratedKeys = isReturnGeneratedKeys;

    if (_isReturnGeneratedKeys && _generatedKeys != null) {
      _generatedKeys.init();
    }
  }

  /**
   * The max rows returned in a select
   */
  public void setLimit(int limit)
  {
    _limit = limit;
  }

  /**
   * The max rows returned in a select
   */
  public int getLimit()
  {
    return _limit;
  }

  /**
   * Sets the indexed group field.
   */
  public boolean isGroupNull(int index)
  {
    return _groupItem.isNull(index);
  }

  /**
   * Sets the indexed group field.
   */
  public void setGroupString(int index, String value)
  {
    _groupItem.setString(index, value);
  }

  /**
   * Sets the indexed group field.
   */
  public String getGroupString(int index)
  {
    String value = _groupItem.getString(index);

    return value;
  }

  /**
   * Sets the indexed group field as a long.
   */
  public void setGroupLong(int index, long value)
  {
    _groupItem.setLong(index, value);
  }

  /**
   * Sets the indexed group field as a long.
   */
  public long getGroupLong(int index)
  {
    return _groupItem.getLong(index);
  }

  /**
   * Sets the indexed group field as a double.
   */
  public void setGroupDouble(int index, double value)
  {
    _groupItem.setDouble(index, value);
  }

  /**
   * Sets the indexed group field as a double.
   */
  public double getGroupDouble(int index)
  {
    return _groupItem.getDouble(index);
  }

  /**
   * Returns the indexed group field.
   */
  public Data getGroupData(int index)
  {
    return _groupItem.getData(index);
  }

  /**
   * Set a null parameter.
   */
  public void setNull(int index)
  {
    _parameters[index - 1].setString(null);
  }

  /**
   * Returns the null parameter.
   */
  public boolean isNull(int index)
  {
    return _parameters[index - 1].isNull();
  }

  /**
   * Set a long parameter.
   */
  public void setLong(int index, long value)
  {
    _parameters[index - 1].setLong(value);
  }

  /**
   * Returns the boolean parameter.
   */
  public int getBoolean(int index)
  {
    return _parameters[index - 1].getBoolean();
  }

  /**
   * Set a boolean parameter.
   */
  public void setBoolean(int index, boolean value)
  {
    _parameters[index - 1].setBoolean(value);
  }

  /**
   * Returns the long parameter.
   */
  public long getLong(int index)
  {
    return _parameters[index - 1].getLong();
  }

  /**
   * Returns the date parameter.
   */
  public long getDate(int index)
  {
    return _parameters[index - 1].getDate();
  }

  /**
   * Returns the date parameter.
   */
  public void setDate(int index, long date)
  {
    _parameters[index - 1].setDate(date);
  }

  /**
   * Set a double parameter.
   */
  public void setDouble(int index, double value)
  {
    _parameters[index - 1].setDouble(value);
  }

  /**
   * Returns the double parameter.
   */
  public double getDouble(int index)
  {
    return _parameters[index - 1].getDouble();
  }

  /**
   * Set a string parameter.
   */
  public void setString(int index, String value)
  {
    _parameters[index - 1].setString(value);
  }

  /**
   * Returns the string parameter.
   */
  public String getString(int index)
  {
    return _parameters[index - 1].getString();
  }

  public boolean isBinaryStream(int index)
  {
    return _parameters[index - 1].isBinaryStream();
  }

  /**
   * Set a binary stream parameter.
   */
  public void setBinaryStream(int index, InputStream is, int length)
  {
    _parameters[index - 1].setBinaryStream(is, length);
  }

  /**
   * Returns the binary stream parameter.
   */
  public InputStream getBinaryStream(int index)
  {
    return _parameters[index - 1].getBinaryStream();
  }

  /**
   * Set a binary stream parameter.
   */
  public void setBytes(int index, byte []bytes)
  {
    _parameters[index - 1].setBytes(bytes);
  }

  /**
   * Returns the binary stream parameter.
   */
  public byte []getBytes(int index)
  {
    return _parameters[index - 1].getBytes();
  }

  public ColumnType getType(int index)
  {
    return _parameters[index - 1].getType();
  }

  /**
   * Sets the result set.
   */
  public void setResult(SelectResult result)
  {
    _result = result;
  }

  /**
   * Gets the result set.
   */
  public SelectResult getResult()
  {
    return _result;
  }

  /**
   * Gets the generated keys result set.
   */
  public GeneratedKeysResultSet getGeneratedKeysResultSet()
  {
    if (! _isReturnGeneratedKeys)
      return null;

    if (_generatedKeys == null)
      _generatedKeys = new GeneratedKeysResultSet();

    return _generatedKeys;
  }

  /**
   * Lock the blocks.  The blocks are locked in ascending block id
   * order to avoid deadlocks.
   *
   * @param isWrite if true, the block should be locked for writing
   */
  public void lock()
    throws SQLException
  {
    if (_isNonLocking)
      return;
    
    if (_isLocked) {
      throw new IllegalStateException(L.l("blocks are already locked"));
    }
    _isLocked = true;

    if (_thread != Thread.currentThread())
      throw new IllegalStateException();

    int len = _tableIterators.length;

    for (int i = 0; i < len; i++) {
      Block bestBlock = null;
      long bestId = Long.MAX_VALUE;

      loop:
      for (int j = 0; j < len; j++) {
        TableIterator iter = _tableIterators[j];

        if (iter == null)
          continue;

        Block block = iter.getBlock();

        if (block == null)
          continue;

        long id = block.getBlockId();
        if (bestId <= id)
          continue;

        for (int k = 0; k < i; k++) {
          if (_blockLocks[k] == block)
            continue loop;
        }

        bestId = id;
        bestBlock = block;
      }

      try {
        if (bestBlock == null) {
        }
        else if (_isWrite) {
          bestBlock.getWriteLock().tryLock(_xa.getTimeout(), TimeUnit.MILLISECONDS);
        }
        else {
          bestBlock.getReadLock().tryLock(_xa.getTimeout(), TimeUnit.MILLISECONDS);
        }
      } catch (Exception e) {
        throw new IllegalStateException(e);
      }
      
      // assignment must be after obtaining lock because the unlock
      // requires a lock
      _blockLocks[i] = bestBlock;
    }
  }

  /**
   * Unlock the blocks.  The blocks are unlocked in descending order.
   *
   * @param isWrite if true, the block should be unlocked for writing
   */
  public void unlock()
    throws SQLException
  {
    if (_isNonLocking)
      return;
    
    if (! _isLocked) {
      return;
    }

    _isLocked = false;

    if (_thread != null && _thread != Thread.currentThread())
      throw new IllegalStateException(String.valueOf(_thread) + " current " + Thread.currentThread());

    int len = _blockLocks.length;

    // need to unlock first since the writeData/commit will wait for
    // write locks to clear before committing
    for (int i = len - 1; i >= 0; i--) {
      Block block = _blockLocks[i];

      if (block == null) {
      }
      else if (_isWrite) {
        block.getWriteLock().unlock();
      }
      else {
        block.getReadLock().unlock();
      }
    }

    try {
      _xa.writeData();
    } finally {
      for (int i = len - 1; i >= 0; i--) {
        Block block = _blockLocks[i];
        _blockLocks[i] = null;

        if (block == null) {
        }
        else if (_isWrite) {
          try {
            block.commit();
          } catch (Exception e) {
            log.log(Level.FINE, e.toString(), e);
          }
        }
      }
    }
  }

  public void close()
    throws SQLException
  {
    Thread thread = _thread;
    _thread = null;

    unlock();

    if (thread != null && thread != Thread.currentThread()) {
      throw new IllegalStateException();
    }
    
    DbTransaction xa = _xa;
    _xa = null;
    
    // db/0a10
    if (xa != null && xa.isAutoCommit())
      xa.commit();
  }

  public static void free(QueryContext cxt)
  {
    _freeList.free(cxt);
  }
}
