/*
 * 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.jms.file;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sql.DataSource;

import com.caucho.config.ConfigException;
import com.caucho.db.jdbc.DataSourceImpl;
import com.caucho.env.service.RootDirectorySystem;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.loader.Environment;
import com.caucho.loader.EnvironmentLocal;
import com.caucho.management.server.AbstractManagedObject;
import com.caucho.management.server.FileQueueStoreMXBean;
import com.caucho.server.cluster.ServletService;
import com.caucho.util.FreeList;
import com.caucho.util.JdbcUtil;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.TempOutputStream;

/**
 * Implements a file queue.
 */
public class FileQueueStore
{
  private static final L10N L = new L10N(FileQueueStore.class);
  private static final Logger log
    = Logger.getLogger(FileQueueStore.class.getName());

  private static final EnvironmentLocal<FileQueueStore> _localStore
    = new EnvironmentLocal<FileQueueStore>();

  // private static final MessageType []MESSAGE_TYPE = MessageType.values();

  private static final int START_LIMIT = 8192;

  private FreeList<StoreConnection> _freeList
    = new FreeList<StoreConnection>(32);

  private DataSource _db;
  private String _queueTable;
  private String _messageTable;
  private FileQueueStoreAdmin _admin;

  public FileQueueStore(Path path, String serverId, ClassLoader loader)
  {
    this(path, serverId, loader, false);
  }

  private FileQueueStore(Path path, String serverId, ClassLoader loader,
                         boolean isServer)
  {
    //    _messageFactory = new MessageFactory();

    init(path, serverId, loader, isServer);
  }

  public FileQueueStore(Path path, String serverId)
  {
    this(path, serverId, Thread.currentThread().getContextClassLoader(), false);
  }

  public static FileQueueStore create()
  {
    ServletService server = ServletService.getCurrent();

    if (server == null)
      throw new IllegalStateException(L.l("FileQueueStore requires an active Resin instance"));

    ClassLoader loader = server.getClassLoader();

    synchronized (_localStore) {
      FileQueueStore store = _localStore.getLevel(loader);

      if (store == null) {
        Path path = RootDirectorySystem.getCurrentDataDirectory();
        String serverId = server.getServerId();

        store = new FileQueueStore(path, serverId, loader, true);

        _localStore.set(store, loader);
      }

      return store;
    }
  }

  private void init(Path path, String serverId, ClassLoader loader,
                    boolean isServer)
  {
    if (path == null)
      throw new NullPointerException();
    
    if (serverId == null)
      throw new NullPointerException();

    try {
      path.mkdirs();
    } catch (IOException e) {
      log.log(Level.ALL, e.toString(), e);
    }

    if (! path.isDirectory())
      throw new ConfigException(L.l("FileQueue requires a valid persistent directory {0}.",
                                    path.getURL()));
    
    if ("".equals(serverId))
      serverId = "default";

    _queueTable = escapeName("jms_queue_" + serverId);
    _messageTable = escapeName("jms_message_" + serverId);
    
    Environment.addCloseListener(this, loader);

    try {
      DataSourceImpl db = new DataSourceImpl(path);
      db.setRemoveOnError(true);
      db.init();

      _db = db;

      Connection conn = _db.getConnection();

      initDatabase(conn);
      
      conn.close();
      
      if (isServer)
        _admin = new FileQueueStoreAdmin();
    } catch (SQLException e) {
      throw ConfigException.create(e);
    }
  }

  /**
   * Adds a new message to the persistent store.
   */
  public long send(byte []queueHash,
                   String msgId,
                   Serializable payload,
                   int priority,
                   long expireTime)
  {
    StoreConnection conn = null;
    
    try {
      TempOutputStream os = new TempOutputStream();
      
      Hessian2Output out = new Hessian2Output(os);
      out.writeObject(payload);
      out.close();
    
      conn = getConnection();

      PreparedStatement sendStmt = conn.prepareSend();

      sendStmt.setBytes(1, queueHash);
      sendStmt.setString(2, msgId);
      sendStmt.setBinaryStream(3, os.openInputStream(), 0);
      sendStmt.setInt(4, priority);
      sendStmt.setLong(5, expireTime);

      sendStmt.executeUpdate();

      if (log.isLoggable(Level.FINE))
        log.fine(this + " send " + payload);

      ResultSet rs = sendStmt.getGeneratedKeys();

      if (! rs.next())
        throw new java.lang.IllegalStateException();

      long id = rs.getLong(1);

      rs.close();
      
      return id;
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      freeConnection(conn, true);
    }
  }

  /**
   * Retrieves a message from the persistent store.
   */
  boolean receiveStart(byte []queueHash, FileQueueImpl<?> fileQueue)
  {
    StoreConnection conn = null;
    boolean isValid = false;
    
    try {
      conn = getConnection();

      PreparedStatement receiveStartStmt = conn.prepareReceiveStart();

      receiveStartStmt.setBytes(1, queueHash);

      ResultSet rs = receiveStartStmt.executeQuery();
      int count = 0;

      while (rs.next()) {
        count++;
        long id = rs.getLong(1);
        String msgId = rs.getString(2);
        int priority = rs.getInt(3);
        long expire = rs.getLong(4);

        fileQueue.addEntry(id, msgId, -1, priority, expire, null);
      }

      rs.close();
      
      isValid = true;

      return count < START_LIMIT;
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      freeConnection(conn, isValid);
    }
  }

  /**
   * Retrieves a message from the persistent store.
   */
  public Serializable readMessage(long id)
  {
    StoreConnection conn = null;
    boolean isValid = false;

    try {
      conn = getConnection();

      PreparedStatement readStmt = conn.prepareRead();

      readStmt.setLong(1, id);

      ResultSet rs = readStmt.executeQuery();

      if (rs.next()) {
        Serializable payload = null;

        InputStream is = rs.getBinaryStream(1);
        if (is != null) {
          Hessian2Input in = new Hessian2Input(is);

          payload = (Serializable) in.readObject();

          in.close();
          is.close();
        }

        return payload;
      }

      rs.close();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      freeConnection(conn, isValid);
    }

    return null;
  }

  /**
   * Retrieves a message from the persistent store.
   */
  public Serializable receive(byte []queueHash)
  {
    StoreConnection conn = null;
    boolean isValid = false;
    
    try {
      conn = getConnection();

      PreparedStatement receiveStmt = conn.prepareReceive();

      receiveStmt.setBytes(1, queueHash);

      ResultSet rs = receiveStmt.executeQuery();

      if (rs.next()) {
        long id = rs.getLong(1);

        rs.close();

        PreparedStatement deleteStmt = conn.prepareDelete();

        deleteStmt.setLong(1, id);

        deleteStmt.executeUpdate();

        return null;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      freeConnection(conn, isValid);
    }

    return null;
  }

  /**
   * Retrieves a message from the persistent store.
   */
  void delete(long id)
  {
    StoreConnection conn = null;
    boolean isValid = false;
    
    try {
      conn = getConnection();

      PreparedStatement deleteStmt = conn.prepareDelete();
      
      deleteStmt.setLong(1, id);

      deleteStmt.executeUpdate();
      
      isValid = true;
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      freeConnection(conn, isValid);
    }
  }

  private void initDatabase(Connection conn)
    throws SQLException
  {
    String sql = ("select id, priority, payload, is_valid"
                  + " from " + _messageTable + " where 1=0");
    
    Statement stmt = conn.createStatement();

    try {
      ResultSet rs = stmt.executeQuery(sql);

      rs.close();
      
      return;
    } catch (SQLException e) {
      log.finer(e.toString());
    }

    try {
      stmt.executeUpdate("drop table " + _queueTable);
    } catch (SQLException e) {
      log.finer(e.toString());
    }

    try {
      stmt.executeUpdate("drop table " + _messageTable);
    } catch (SQLException e) {
      log.finer(e.toString());
    }

    sql = ("create table " + _queueTable + " ("
           + "  id bigint primary key auto_increment,"
           + "  name varchar(128)"
           + ")");

    stmt.executeUpdate(sql);

    sql = ("create table " + _messageTable + " ("
           + "  id identity primary key,"
           + "  queue_id binary(32),"
           + "  priority integer,"
           + "  expire datetime,"
           + "  msg_id varchar(64),"
           + "  payload blob,"
           + "  is_valid bit"
           + ")");

    stmt.executeUpdate(sql);
  }

  public int getMessageCount()
  {
    Connection conn = null;
    
    try {
      conn = _db.getConnection();
      
      String sql = "select count(*) from " + _messageTable;
      
      Statement stmt = conn.createStatement();
      
      ResultSet rs = stmt.executeQuery(sql);
      
      if (rs.next()) {
        return rs.getInt(1);
      }
      else {
        return -1;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      JdbcUtil.close(conn);
    }
  }
  
  public void close()
  {
    if (_admin != null)
      _admin.close();
  }

  private StoreConnection getConnection()
    throws SQLException
  {
    StoreConnection storeConn = _freeList.allocate();

    if (storeConn != null) {
      return storeConn;
    } else {
      Connection conn = _db.getConnection();

      return new StoreConnection(conn);
    }
  }

  private void freeConnection(StoreConnection conn, boolean isValid)
  {
    if (conn == null) {     
    } else if (isValid) {
      _freeList.free(conn);
    }
    else
      conn.close();
  }

  private static String escapeName(String name)
  {
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < name.length(); i++) {
      char ch = name.charAt(i);

      if ('a' <= ch && ch <= 'z'
          || 'A' <= ch && ch <= 'Z'
          || '0' <= ch && ch <= '0'
          || ch == '_') {
        sb.append(ch);
      }
      else
        sb.append('_');
    }

    return sb.toString();
  }

  public String toString()
  {
    return getClass().getSimpleName() + "[" + _messageTable + "]";
  }

  class StoreConnection {
    private Connection _conn;

    private PreparedStatement _sendStmt;
    private PreparedStatement _receiveStartStmt;
    private PreparedStatement _readStmt;
    private PreparedStatement _receiveStmt;
    private PreparedStatement _removeStmt;
    private PreparedStatement _deleteStmt;

    StoreConnection(Connection conn)
    {
      _conn = conn;
    }

    PreparedStatement prepareSend()
      throws SQLException
    {
      if (_sendStmt == null) {
        String sql = ("insert into " + _messageTable
                      + " (queue_id,msg_id,payload,priority,expire,is_valid)"
                      + " VALUES(?,?,?,?,?,1)");
    
        _sendStmt = _conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
      }

      return _sendStmt;
    }

    PreparedStatement prepareReceive()
      throws SQLException
    {
      if (_receiveStmt == null) {
        String sql = ("select id,msg_id,payload from " + _messageTable
                      + " WHERE queue_id=? LIMIT 1");
    
        _receiveStmt = _conn.prepareStatement(sql);
      }

      return _receiveStmt;
    }

    PreparedStatement prepareRead()
      throws SQLException
    {
      if (_readStmt == null) {
        String sql = ("select payload from " + _messageTable
                      + " WHERE id=?");
    
        _readStmt = _conn.prepareStatement(sql);
      }

      return _readStmt;
    }

    PreparedStatement prepareReceiveStart()
      throws SQLException
    {
      if (_receiveStartStmt == null) {
        String sql = ("select id,msg_id,priority,expire"
                      + " from " + _messageTable
                      + " WHERE queue_id=? AND is_valid=1" // ORDER BY id"
                      + " LIMIT " + START_LIMIT);
    
        _receiveStartStmt = _conn.prepareStatement(sql);
      }

      return _receiveStartStmt;
    }
    
    PreparedStatement prepareRemove()
      throws SQLException
    {
      if (_removeStmt == null) {
        String sql = ("update " + _messageTable
                      + " set payload=null, is_valid=0, expire=now() + 120000"
                      + " WHERE id=?");
    
        _removeStmt = _conn.prepareStatement(sql);
      }

      return _removeStmt;
    }

    PreparedStatement prepareDelete()
      throws SQLException
    {
      if (_deleteStmt == null) {
        String sql = ("delete from " + _messageTable
                      + " WHERE id=?");
    
        _deleteStmt = _conn.prepareStatement(sql);
      }

      return _deleteStmt;
    }
    
    void close()
    {
      try {
        Connection conn = _conn;
        _conn = null;
        
        if (conn != null)
          conn.close();
      } catch (SQLException e) {
        log.log(Level.FINER, e.toString(), e);
      }
    }
  }
  
  class FileQueueStoreAdmin extends AbstractManagedObject 
    implements FileQueueStoreMXBean 
  {
    FileQueueStoreAdmin()
    {
      registerSelf();
    }
    
    public void close()
    {
      unregisterSelf();
    }
    
    @Override
    public long getMessageCount()
    {
      // TODO Auto-generated method stub
      return 0;
    }

    @Override
    public String getName()
    {
      return null;
    }
  }
}
