/*
 * 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.db.sql;

import com.caucho.db.store.BlobInputStream;
import com.caucho.db.store.Store;
import com.caucho.db.table.Column;
import com.caucho.db.table.TableIterator;
import com.caucho.sql.SQLExceptionWrapper;
import com.caucho.util.CharBuffer;
import com.caucho.util.FreeList;
import com.caucho.util.IntArray;
import com.caucho.util.L10N;
import com.caucho.util.QDate;
import com.caucho.vfs.TempBuffer;

import java.io.IOException;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;

public class SelectResult {
  private static final L10N L = new L10N(SelectResult.class);
  
  private static final FreeList<SelectResult> _freeList
    = new FreeList<SelectResult>(32);

  private static final int SIZE = TempBuffer.SIZE;
  
  private static QDate _date = new QDate();

  private CharBuffer _cb = new CharBuffer();
  private byte []_blob = new byte[128];
  
  private Expr []_exprs;
  private Store []_stores = new Store[32];

  private TableIterator []_rows = new TableIterator[16];

  private Order _order;
  private IntArray _orderIndex;
  
  private TempBuffer []_tempBuffers = new TempBuffer[128];
  private byte [][]_buffers = new byte[128][];
  private int _length;
  private int _rowCount;

  private int _row;
  
  private int _offset;
  private int _rowOffset;
  private int _columnOffset;
  private int _column;
  
  private boolean _wasNull;

  private SelectResult()
  {
  }

  public static SelectResult create(Expr []exprs, Order order)
  {
    SelectResult rs = _freeList.allocate();

    if (rs == null)
      rs = new SelectResult();

    rs.init(exprs, order);

    return rs;
  }

  /**
   * Initialize the iterator.
   */
  TableIterator []initRows(FromItem []fromItems)
  {
    if (_rows.length < fromItems.length)
      _rows = new TableIterator[fromItems.length];
      
    for (int i = 0; i < fromItems.length; i++) {
      if (_rows[i] == null)
	_rows[i] = new TableIterator();
      _rows[i].init(fromItems[i].getTable());
    }

    return _rows;
  }
    
  /**
   * Initialize based on the exprs.
   */
  private void init(Expr []exprs, Order order)
  {
    _exprs = exprs;
    _order = order;

    if (order != null)
      _orderIndex = new IntArray();
    
    if (_stores.length < _exprs.length) {
      _stores = new Store[exprs.length];
    }

    for (int i = 0; i < exprs.length; i++)
      _stores[i] = exprs[i].getTable();

    _length = 0;
    _rowCount = 0;
  }

  void initRead()
    throws SQLException
  {
    if (_order != null)
      _order.sort(this, _orderIndex);

    _row = -1;
    _offset = 0;
    _column = 0;
    _rowOffset = 0;
    _columnOffset = 0;
  }

  /**
   * Moves to the next row, returning true if the row has data.
   */
  public boolean next()
    throws SQLException
  {
    if (++_row < _rowCount) {
      if (_orderIndex != null) {
	_offset = _orderIndex.get(_row);
      }
      else if (_row != 0) {
	_offset = _columnOffset;
	skipColumns(_exprs.length - _column);
      }
      
      _column = 0;
      _rowOffset = _offset;
      _columnOffset = _rowOffset;

      return true;
    }
    else
      return false;
  }

  /**
   * Returns the expressions.
   */
  public Expr []getExprs()
  {
    return _exprs;
  }

  /**
   * Returns the column index with the given name.
   */
  public int findColumnIndex(String name)
    throws SQLException
  {
    for (int i = 0; i < _exprs.length; i++) {
      if (_exprs[i].getName().equals(name))
	return i + 1;
    }

    throw new SQLException(L.l("column `{0}' does not exist.", name));
  }

  /**
   * Returns the string value of the given index.
   */
  public String getString(int index)
    throws SQLException
  {
    _wasNull = false;

    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return null;
      
    case Column.SHORT:
      {
	int value = (short) ((read() << 8) + (read()));

	return String.valueOf(value);
      }
      
    case Column.INT:
      {
	int value = ((read() << 24) +
		     (read() << 16) +
		     (read() << 8) +
		     (read()));

	return String.valueOf(value);
      }
      
    case Column.LONG:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return String.valueOf(value);
      }
      
    case Column.DOUBLE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return String.valueOf(Double.longBitsToDouble(value));
      }
      
    case Column.DATE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return QDate.formatISO8601(value);
      }

    case Column.VARCHAR:
      return readString();

    case Column.BLOB:
      return readBlobString();

    default:
      throw new RuntimeException("unknown column type:" + type + " column:" + index);
    }
  }

  /**
   * Returns the integer value of the column.
   */
  public int getInt(int index)
    throws SQLException
  {
    _wasNull = false;
    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return 0;
      
    case Column.SHORT:
      {
	int value = (short) ((read() << 8)
			     + (read()));

	return value;
      }
      
    case Column.INT:
      {
	int value = ((read() << 24) +
		     (read() << 16) +
		     (read() << 8) +
		     (read()));

	return value;
      }
      
    case Column.LONG:
    case Column.DATE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return (int) value;
      }
      
    case Column.DOUBLE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return (int) Double.longBitsToDouble(value);
      }

    case Column.VARCHAR:
      return Integer.parseInt(readString());

    case Column.BLOB:
      return Integer.parseInt(readBlobString());

    default:
      return 0;
    }
  }

  /**
   * Returns the long value of the column.
   */
  public long getLong(int index)
    throws SQLException
  {
    _wasNull = false;
    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return 0;
      
    case Column.SHORT:
      {
	int value = (short) ((read() << 8)
			     + (read()));

	return value;
      }
      
    case Column.INT:
      {
	int value = ((read() << 24) +
		     (read() << 16) +
		     (read() << 8) +
		     (read()));

	return value;
      }
      
    case Column.LONG:
    case Column.DATE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return value;
      }
      
    case Column.DOUBLE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return (long) Double.longBitsToDouble(value);
      }

    case Column.VARCHAR:
      return Long.parseLong(readString());

    case Column.BLOB:
      return Long.parseLong(readBlobString());

    default:
      return 0;
    }
  }

  /**
   * Returns a double value from this column.
   */
  public double getDouble(int index)
    throws SQLException
  {
    _wasNull = false;
    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return 0;
      
    case Column.SHORT:
      {
	int value = (short) ((read() << 8)
			     + (read()));

	return value;
      }
      
    case Column.INT:
      {
	int value = ((read() << 24) +
		     (read() << 16) +
		     (read() << 8) +
		     (read()));

	return value;
      }
      
    case Column.LONG:
    case Column.DATE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return value;
      }
      
    case Column.DOUBLE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return Double.longBitsToDouble(value);
      }

    case Column.VARCHAR:
      return Double.parseDouble(readString());

    case Column.BLOB:
      return Double.parseDouble(readBlobString());

    default:
      return 0;
    }
  }

  public long getDate(int index)
    throws SQLException
  {
    _wasNull = false;
    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return 0;
      
    case Column.LONG:
    case Column.DATE:
      {
	long value = (((long) read() << 56) +
		      ((long) read() << 48) +
		      ((long) read() << 40) +
		      ((long) read() << 32) +
		      ((long) read() << 24) +
		      ((long) read() << 16) +
		      ((long) read() << 8) +
		      ((long) read()));
	
	return value;
      }

    case Column.VARCHAR:
      {
	String value = readString();
	
	synchronized (_date) {
	  try {
	    return _date.parseDate(value);
	  } catch (Exception e) {
	    throw new SQLExceptionWrapper(e);
	  }
	}
      }

    case Column.BLOB:
      {
	String value = readBlobString();
	
	synchronized (_date) {
	  try {
	    return _date.parseDate(value);
	  } catch (Exception e) {
	    throw new SQLExceptionWrapper(e);
	  }
	}
      }

    default:
      throw new SQLException("unknown type: " + type);
    }
  }

  /**
   * Returns the blob value of the given index.
   */
  public Blob getBlob(int index)
    throws SQLException
  {
    _wasNull = false;

    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return null;

    case Column.BLOB:
      return getBlob();

    default:
      throw new RuntimeException("column can't be retrieved as a blob:" + type + " column:" + index);
    }
  }

  /**
   * Returns the clob value of the given index.
   */
  public Clob getClob(int index)
    throws SQLException
  {
    _wasNull = false;

    setColumn(index);

    int type = read();
    switch (type) {
    case Column.NONE:
      _wasNull = true;
      return null;

    case Column.BLOB:
      return getClob();

    default:
      throw new RuntimeException("column can't be retrieved as a clob:" + type + " column:" + index);
    }
  }

  /**
   * Returns true if the last column read was null.
   */
  public boolean wasNull()
  {
    return _wasNull;
  }

  /**
   * Returns the string value for the result set.
   */
  private String readString()
    throws SQLException
  {
    int length = ((read() << 24) +
		  (read() << 16) +
		  (read() << 8) +
		  (read()));

    int len = length >> 1;

    CharBuffer cb = _cb;
    cb.ensureCapacity(len);
    char []cBuf = cb.getBuffer();
    int cLen = 0;

    for (; len > 0; len--) {
      int ch1 = read();
      int ch2 = read();

      cBuf[cLen++] = (char) (((ch1 & 0xff) << 8) + (ch2 & 0xff));
    }

    return new String(cBuf, 0, cLen);
  }

  /**
   * Returns the blob value for the result set.
   */
  private Blob getBlob()
    throws SQLException
  {
    BlobImpl blob = new BlobImpl();

    blob.setStore(_stores[_column]);

    byte []inode = blob.getInode();

    read(inode, 0, 128);

    return blob;
  }

  /**
   * Returns the clob value for the result set.
   */
  private Clob getClob()
    throws SQLException
  {
    ClobImpl clob = new ClobImpl();

    clob.setStore(_stores[_column]);

    byte []inode = clob.getInode();

    read(inode, 0, 128);

    return clob;
  }

  /**
   * Returns the string value for the result set.
   */
  private String readBlobString()
    throws SQLException
  {
    read(_blob, 0, 128);

    CharBuffer cb = _cb;
    cb.clear();

    BlobInputStream is = null;
    try {
      is = new BlobInputStream(_stores[_column], _blob, 0);

      int ch;
      while ((ch = is.read()) >= 0) {
	if (ch < 0x80)
	  cb.append((char) ch);
      }
    } catch (IOException e) {
      throw new SQLExceptionWrapper(e);
    }

    return cb.toString();
  }

  /**
   * Set the column in the current row.
   */
  private void setColumn(int column)
  {
    if (column < _column) {
      _offset = _rowOffset;
      skipColumns(column);
    }
    else {
      _offset = _columnOffset;
      skipColumns(column - _column);
    }

    _column = column;
    _columnOffset = _offset;
  }

  /**
   * Set the column in the current row.
   */
  void setRow(int rowOffset)
  {
    _rowOffset = rowOffset;
    _offset = rowOffset;
    _column = 0;
    _columnOffset = rowOffset;
  }

  /**
   * Skips the specified number of columns.
   */
  private void skipColumns(int count)
  {
    for (; count > 0; count--) {
      int type = read();
      int sublen;

      switch (type) {
      case Column.NONE:
	break;
	  
      case Column.VARCHAR:
	int l0 = read();
	int l1 = read();
	int l2 = read();
	int l3 = read();
	
	sublen = ((l0 << 24) +
		  (l1 << 16) +
		  (l2 << 8) +
		  (l3));

	_offset += sublen;
	break;

      case Column.SHORT:
	_offset += 2;
	break;
      case Column.INT:
	_offset += 4;
	break;
      case Column.LONG:
      case Column.DOUBLE:
      case Column.DATE:
	_offset += 8;
	break;
	  
      case Column.BLOB:
	_offset += 128;
	break;
	  
      default:
	throw new RuntimeException("Unknown column: " + type);
      }
    }
  }

  /**
   * Starts a row
   */
  public void startRow()
  {
    if (_orderIndex != null)
      _orderIndex.add(_length);

    _rowCount++;
  }

  /**
   * Writes a null.
   */
  public void writeNull()
  {
    write(Column.NONE);
  }

  /**
   * Writes a string.
   */
  public void writeString(String s)
  {
    write(Column.VARCHAR);
    int stringLength = s.length();
    int length = 2 * stringLength;
    write(length >> 24);
    write(length >> 16);
    write(length >> 8);
    write(length);

    for (int i = 0; i < stringLength; i++) {
      char ch = s.charAt(i);

      write(ch << 8);
      write(ch);
    }
  }

  /**
   * Writes a string.
   */
  public void writeString(byte []buffer, int offset, int stringLength)
  {
    int rLength = _length;
    
    int rOffset = rLength % SIZE;
    int rBlockId = rLength / SIZE;
    
    if (_buffers[rBlockId] == null) {
      TempBuffer tempBuffer = TempBuffer.allocate();
      _tempBuffers[rBlockId] = tempBuffer;
      _buffers[rBlockId] = tempBuffer.getBuffer();
    }

    byte []rBuffer = _buffers[rBlockId];
    rBuffer[rOffset] = Column.VARCHAR;

    int length = 2 * stringLength;

    if (rOffset + 5 < rBuffer.length) {
      rBuffer[rOffset + 1] = (byte) (length >> 24);
      rBuffer[rOffset + 2] = (byte) (length >> 16);
      rBuffer[rOffset + 3] = (byte) (length >> 8);
      rBuffer[rOffset + 4] = (byte) length;

      if (rOffset + 5 + length < SIZE) {
	System.arraycopy(buffer, offset, rBuffer, rOffset + 5, length);

	_length = rLength + 5 + length;
      }
      else {
	_length = rLength + 5;
	write(buffer, offset, length);
      }
    }
    else {
      _length = rLength + 1;

      write(length >> 24);
      write(length >> 16);
      write(length >> 8);
      write(length);

      write(buffer, offset, length);
    }
  }

  /**
   * Writes a string.
   */
  public void writeBlock(int code, byte []buffer, int offset, int length)
  {
    write(code);
    write(buffer, offset, length);
  }

  /**
   * Writes a double.
   */
  public void writeDouble(double dValue)
  {
    write(Column.DOUBLE);

    long value = Double.doubleToLongBits(dValue);
    
    write((int) (value >> 56));
    write((int) (value >> 48));
    write((int) (value >> 40));
    write((int) (value >> 32));
    write((int) (value >> 24));
    write((int) (value >> 16));
    write((int) (value >> 8));
    write((int) value);
  }

  /**
   * Writes a long.
   */
  public void writeLong(long value)
  {
    write(Column.LONG);
    write((int) (value >> 56));
    write((int) (value >> 48));
    write((int) (value >> 40));
    write((int) (value >> 32));
    write((int) (value >> 24));
    write((int) (value >> 16));
    write((int) (value >> 8));
    write((int) value);
  }

  /**
   * Writes a date.
   */
  public void writeDate(long value)
  {
    write(Column.DATE);
    write((int) (value >> 56));
    write((int) (value >> 48));
    write((int) (value >> 40));
    write((int) (value >> 32));
    write((int) (value >> 24));
    write((int) (value >> 16));
    write((int) (value >> 8));
    write((int) value);
  }

  /**
   * Writes an long.
   */
  public void writeInt(int value)
  {
    write(Column.INT);
    write(value >> 24);
    write(value >> 16);
    write(value >> 8);
    write(value);
  }

  /**
   * Writes a short
   */
  public void writeShort(int value)
  {
    write(Column.SHORT);
    write(value >> 8);
    write(value);
  }

  /**
   * Writes a blob.
   */
  public void writeBlob(byte []buffer, int offset)
  {
    write(Column.BLOB);

    write(buffer, offset, 128);
  }

  /**
   * Seeks the specified offset.
   */
  private void seek(int offset)
  {
    _offset = offset;
  }

  /**
   * Reads the next byte.
   */
  private int read()
  {
    int offset = _offset;
    
    if (_length <= offset)
      return -1;

    _offset = offset + 1;
    
    byte []buf = _buffers[offset / SIZE];

    return buf[offset % SIZE] & 0xff;
  }

  /**
   * Reads the next byte.
   */
  private int read(byte []buffer, int bufOffset, int bufLength)
  {
    int offset = _offset;
    int length = _length;
    byte [][]buffers = _buffers;

    for (int i = bufLength; i > 0; i--) {
      if (length <= offset) {
	_offset = offset;
	return -1;
      }

      byte []buf = buffers[offset / SIZE];

      buffer[bufOffset] = buf[offset % SIZE];
      
      offset++;
      bufOffset++;
    }

    _offset = offset;

    return bufLength;
  }

  /**
   * Writes the next byte.
   */
  public void write(int value)
  {
    int length = _length;
    int rOffset = length % SIZE;
    int blockId = length / SIZE;

    while (_buffers.length <= blockId) {
      byte [][]newBuffers = new byte[2 * _buffers.length][];
      
      System.arraycopy(_buffers, 0, newBuffers, 0, _buffers.length);
      _buffers = newBuffers;
      
      TempBuffer []newTempBuffers = new TempBuffer[newBuffers.length];
      System.arraycopy(_tempBuffers, 0, newTempBuffers, 0,
		       _tempBuffers.length);
      _tempBuffers = newTempBuffers;
    }
    
    byte []buffer = _buffers[blockId];
    
    if (buffer == null) {
      TempBuffer tempBuffer = TempBuffer.allocate();
      _tempBuffers[blockId] = tempBuffer;
      _buffers[blockId] = tempBuffer.getBuffer();

      buffer = _buffers[blockId];
    }

    buffer[rOffset] = (byte) value;

    _length = length + 1;
  }

  /**
   * Writes a buffer
   */
  public void write(byte []buffer, int offset, int length)
  {
    int rLength = _length;
    
    while (length > 0) {
      int rOffset = rLength % SIZE;

      int rBufferId = rLength / SIZE;
      
      if (rOffset == 0) {
	TempBuffer tempBuffer = TempBuffer.allocate();
	if (_tempBuffers.length <= rBufferId) {
	  int len = _tempBuffers.length;
	  
	  TempBuffer []newTempBuffers = new TempBuffer[len + 32];
	  System.arraycopy(_tempBuffers, 0, newTempBuffers, 0, len);
	  _tempBuffers = newTempBuffers;
	  
	  byte [][]newBuffers = new byte[len + 32][];
	  System.arraycopy(_buffers, 0, newBuffers, 0, len);
	  _buffers = newBuffers;

	}
	
	_tempBuffers[rBufferId] = tempBuffer;
	_buffers[rBufferId] = tempBuffer.getBuffer();
      }

      byte []rBuffer = _buffers[rBufferId];

      int sublen = rBuffer.length - rOffset;

      if (length < sublen)
	sublen = length;

      System.arraycopy(buffer, offset, rBuffer, rOffset, sublen);

      length -= sublen;
      offset += sublen;
      rLength += sublen;
    }

    _length = rLength;
  }
  
  public void close()
  {
    for (int i = 0; i < _buffers.length; i++) {
      TempBuffer buffer = _tempBuffers[i];

      if (buffer != null)
	TempBuffer.free(buffer);

      _tempBuffers[i] = null;
      _buffers[i] = null;
    }

    _order = null;
    _orderIndex = null;
    
    _freeList.free(this);
  }
}
