/*
 * Copyright (c) 1998-2003 Caucho Technology -- all rights reserved
 *
 * Caucho Technology permits redistribution, modification and use
 * of this file in source and binary form ("the Software") under the
 * Caucho Developer Source License ("the License").  The following
 * conditions must be met:
 *
 * 1. Each copy or derived work of the Software must preserve the copyright
 *    notice and this notice unmodified.
 *
 * 2. Redistributions of the Software in source or binary form must include
 *    an unmodified copy of the License, normally in a plain ASCII text
 *
 * 3. The names "Resin" or "Caucho" are trademarks of Caucho Technology and
 *    may not be used to endorse products derived from this software.
 *    "Resin" or "Caucho" may not appear in the names of products derived
 *    from this software.
 *
 * This Software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
 *
 * CAUCHO TECHNOLOGY AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES
 * SUFFERED BY LICENSEE OR ANY THIRD PARTY AS A RESULT OF USING OR
 * DISTRIBUTING SOFTWARE. IN NO EVENT WILL CAUCHO OR ITS LICENSORS BE LIABLE
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE SOFTWARE, EVEN IF HE HAS BEEN ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGES.
 *
 * @author Sam 
 */

package com.caucho.doc.javadoc;

import com.caucho.config.ConfigException;
import com.caucho.config.types.Period;
import com.caucho.loader.Environment;
import com.caucho.log.Log;
import com.caucho.server.webapp.Application;
import com.caucho.server.webapp.PathMapping;
import com.caucho.util.CharBuffer;
import com.caucho.util.Crc64;
import com.caucho.util.L10N;
import com.caucho.vfs.Path;
import com.caucho.vfs.ReadStream;
import com.caucho.vfs.Vfs;
import com.caucho.vfs.WriteStream;

import java.io.IOException;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import javax.sql.DataSource;

/**
 * A store for javadoc information.
 */
public class Store {
  static protected final Logger log = Log.open(Store.class);
  static final L10N L = new L10N(Store.class);

  private final static String STORE_JNDINAME = "resin-javadoc/store";

  private String _dataSource;
  private LinkedHashMap<String,Api> _api = new LinkedHashMap<String,Api>();

  private Path _timestampFile;
  private String _tableNameFile = "javadoc_file";
  private String _tableNameItem = "javadoc_item";
  private long _httpCachePeriod;

  private String _startPage = "overview.jsp";
  private boolean _showAddHelp;

  private long _crc64 = 0;
  private DataSource _pool;

  private Exception _initError;

  /** 
   * A convenience method to do the JNDI lookup
   */
  static public Store getInstance()
    throws NamingException
  {
    Context env = (Context) new InitialContext().lookup("java:comp/env");

    Store store = (Store) env.lookup(STORE_JNDINAME);

    if (store == null)
      throw new NamingException(L.l("`{0}' is an unknown Store",STORE_JNDINAME));
    return store;
  }

  /**
   * The data-source indicates the database to use.
   */
  public void setDataSource(String dataSource)
  {
    _dataSource = dataSource;
  }

  /**
   * A timestamp file is used to determine if the database needs to be
   * recreated from the javadoc, default "WEB-INF/timestamp".
   */
  public void setTimestampFile(Path timestampFile)
  {
    _timestampFile = timestampFile;
  }

  /**
   * An api is a javadoc generated set of html files.
   */
  public void addApi(Api api)
    throws ConfigException
  {
    if (_api.get(api.getId()) != null)
      throw new ConfigException(L.l("id `{0}' already used, id must be unique",api.getId()));
    _api.put(api.getId(),api);
  }

  /**
   * An api is a javadoc generated set of html files.
   */
  public Api getApi(String id)
  {
    return _api.get(id);
  }

  /**
   * An api is a javadoc generated set of html files.
   */
  public Collection<Api> getAllApi()
  {
    return _api.values();
  }

  /**
   * The table name for storing information about javadoc items, the default is
   * "javadoc_item".
   */
  public void setTableNameItem(String tableNameItem)
  {
    _tableNameItem = tableNameItem;
  }
  
  /**
   * The table name for storing information about javadoc items.
   */
  public String getTableNameItem()
  {
    return _tableNameItem;
  }
  
  /**
   * The table name for storing information about javadoc files, the default is
   * "javadoc_file".
   */
  public void setTableNameFile(String tableNameFile)
  {
    _tableNameFile = tableNameFile;
  }
  
  /**
   * The table name for storing information about javadoc files.
   */
  public String getTableNameFile()
  {
    return _tableNameFile;
  }

  /**
   * The time period to indicate to Resin's cache and to browsers
   * that the responses (including search results) should be cached.
   * If Resin's http cache is enabled, then the cached
   * result will be used for subsequent searches for the same thing, even
   * by a different user.  This is an effective way to avoid hitting the
   * database for every search.
   *
   * examples: 10m, 10h, 10D,
   * -1 disables (not a good idea, but useful during development).
   */
  public void setHttpCachePeriod(Period httpCachePeriod)
  {
    _httpCachePeriod = httpCachePeriod.getPeriod();
  }
  
  /**
   * The time period for cache expiry, in ms.
   */
  public long getHttpCachePeriod()
  {
    return _httpCachePeriod;
  }
  
  /**
   * The page to show in the class window for the first request, default
   * "overview.jsp".
   */
  public void setStartPage(String startPage)
  {
    _startPage = startPage;
  }
  
  /**
   * The page to show in the class window for the first request.
   */
  public String getStartPage()
  {
    return _startPage;
  }
  
  /**
   * True/false show a help message about adding more api's.
   */
  public void setShowAddHelp(boolean showAddHelp)
  {
    _showAddHelp = showAddHelp;
  }
  
  /**
   * True/false show a help message about adding more api's.
   */
  public boolean getShowAddHelp()
  {
    return _showAddHelp;
  }
  
  public void init()
  {
    try {
      if (_timestampFile == null)
        _timestampFile = Vfs.lookup("WEB-INF/timestamp");

      try {
        Context env = (Context) new InitialContext().lookup("java:comp/env");

        _pool = (DataSource) env.lookup(_dataSource);

        if (_pool == null)
          throw new ConfigException(L.l("`{0}' is an unknown DataSource, database has not been configured or is not configured properly.",_dataSource));
      } catch (NamingException e) {
        throw new ConfigException(e);
      }

      // update database if needed

      for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
        Api api = i.next();
        _crc64 = api.generateCrc64(_crc64);
      }
      _crc64 = Crc64.generate(_crc64,_dataSource);
      _crc64 = Crc64.generate(_crc64,_timestampFile.toString());
      _crc64 = Crc64.generate(_crc64,_tableNameFile);
      _crc64 = Crc64.generate(_crc64,_tableNameItem);

      try {
        if (isNeedUpdate())
          createFromIndex();
      } catch (Exception ex) {
        throw new ConfigException(ex);
      }

      // add path-mappings to map local files with absolute paths

      Application app = Application.getLocal();

      CharBuffer cb = CharBuffer.allocate();

      for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
        Api api = i.next();

        if (api.isLocalAbsolute()) {
          cb.clear();
          cb.append("/");
          cb.append(api.getId());
          cb.append("/*");
          String urlPattern = cb.toString();

          PathMapping pm = new PathMapping();
          pm.setUrlPattern(urlPattern);
          pm.setRealPath(api.getLocation());
          try {
            app.addPathMapping(pm);
          } catch (Exception ex) {
            throw new ConfigException(ex);
          }
        }
      }

      cb.free();


      // add dependencies to the Environment so that if a local api
      // is regenerated the web-app is restarted

      for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
        Api api = i.next();
        if (api.isLocal()) {
          Path index = api.getLocationPath().lookup("index.html");
          Environment.addDependency(index);
        }
      }
    } catch (Exception ex) {
      log.log(Level.FINE,"resin-javadoc init error",ex);
      _initError = ex;
    }
  }

  public Exception getInitError()
  {
    return _initError;
  }

  /**
   * True if the timestamp file indicates that the database needs updating to
   * match the index file(s).
   */ 
  public boolean isNeedUpdate()
  {
    try {
      ReadStream rs = _timestampFile.openRead();
      String lms = rs.readLine();
      long crc = Long.parseLong(lms);
      lms = rs.readLine();
      long tsfiles = Long.parseLong(lms);
      rs.close();

      if (crc != _crc64) {
        log.info(L.l("javadoc index needs update - config has changed"));
        if (log.isLoggable(Level.FINE))
          log.finer(L.l("timestamp file {0} lastmodified {1}",_timestampFile,new Long(crc)));
      }
      else if (tsfiles != getTimestampForLocalFiles()) {
        log.info(L.l("javadoc index needs update - a local api has changed"));
      }
      else {
        return false;
      }
    } catch (Exception ex) {
      log.info(L.l("javadoc index needs update - timestamp file could not be read"));
      if (log.isLoggable(Level.FINE))
        log.finer(L.l("timestamp file {0} {1}",_timestampFile,ex));
    }

    return true;
  }

  /**
   * Clear the database tables (if they exist) and create new 
   * contents based on the passed javadoc index file(s).
   */ 
  public void createFromIndex()
    throws SQLException, IOException
  {
    long st = System.currentTimeMillis();
    int cnt = 0;

    log.info(L.l("creating javadoc index db entries"));
    Connection conn = null;
    try {
      conn = _pool.getConnection();

      StoreWriter sw = new StoreWriter(this);
      sw.clear(conn);
      for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
        Api api = i.next();
        cnt += sw.add(conn,api);
      }
    } finally {
      try {
        if (conn != null)
          conn.close();
      } catch (SQLException e) {
        log.warning(L.l("conn.close() error",e));
      }
    }

    updateTimestampFile();

    long tm = System.currentTimeMillis() - st;
    log.info(L.l("created {0} javadoc index entries in {1}sec", new Integer(cnt), new Double( (double) tm / 1000.0)));
  }

  private long getTimestampForLocalFiles()
  {
    long ts = 0L;
    for (Iterator<Api> i = _api.values().iterator(); i.hasNext(); ) {
      Api api = i.next();
      if (api.isLocal()) {
        ts += api.getLocationPath().lookup("index.html").getLastModified();
      }
    }

    return ts;
  }

  private void updateTimestampFile()
    throws IOException
  {
    _timestampFile.getParent().mkdirs();
    WriteStream ws = _timestampFile.openWrite();
    ws.println(_crc64);
    ws.println(getTimestampForLocalFiles());
    ws.close();
  }

  /**
   * Look for a javadoc item.  The string can begin with `package', `class',
   * `method', `var', or `any'.  If it begins with none of them, `any' is
   * assumed.  The rest of the string is used as the name to search for.
   * If the name conatins the `*' character, the `*' will match any 
   * characters.
   *
   * @returns a list of the results, a list with size 0 for no results 
   */
  public LinkedList<JavadocItem> query(String query, int offset, int limit)
    throws SQLException 
  {
    LinkedList<JavadocItem> results = new LinkedList<JavadocItem>();

    try {
      int type = -1;

      int i = query.indexOf(' ');
      if (i > -1) {
        String t = query.substring(0,i);
        if (t.equals("package"))
          type = JavadocItem.PACKAGE;
        else if (t.equals("class"))
          type = JavadocItem.CLASS;
        else if (t.equals("method"))
          type = JavadocItem.METHOD;
        else if (t.equals("var"))
          type = JavadocItem.VARIABLE;
        else if (t.equals("any"))
          type = JavadocItem.ANY;

        if (type > -1) {
          while (i < query.length() && Character.isWhitespace(query.charAt(i)))
            i++;
          if (i >= query.length())
            return results;
          else
            query = query.substring(i);
        }
      }


      // handle the special case of ClassName.method
      if (query.length() > 0 && Character.isUpperCase(query.charAt(0))) {
        int di = query.indexOf('.');
        if (di > -1 ) {
          CharBuffer cb = CharBuffer.allocate();
          cb.append('*');
          cb.append(query);
          cb.append('*');
          query = cb.close();
        }
      }

      if (type < 0)
        type = JavadocItem.ANY;

      Connection conn = null;
      try {
        conn = _pool.getConnection();

        Statement stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
        // construct the query or queries
        boolean like = query.indexOf("*") > -1 ? true : false; 
        String nameToUse = (query.indexOf(".") > -1) ? "item.fullname" : "item.name";
        String safequery = escapeSQL(query);

        CharBuffer cb = CharBuffer.allocate();
        cb.append("SELECT item.name,item.fullname,item.typ,item.anchor,item.description,file.api,file.path FROM ");
          cb.append(getTableNameItem());
          cb.append(" AS item, ");
          cb.append(getTableNameFile());
          cb.append(" AS file WHERE file.id = item.file_id");

        if (type != JavadocItem.ANY) {
          cb.append(" AND item.typ = ");
          cb.append(type);
        }
        String q_select = cb.close();

        if (like) { 
          cb = CharBuffer.allocate();
          cb.append(q_select);
          cb.append(" AND ");
          cb.append(nameToUse);
          cb.append(" LIKE '");
          cb.append(safequery);
          cb.append("'");

          results = doQuery(safequery,stmt,cb,offset,limit,results);
        }
        else {
          cb = CharBuffer.allocate();
          cb.append(q_select);
          cb.append(" AND ");
          cb.append(nameToUse);
          cb.append(" LIKE '");
          cb.append(safequery);
          cb.append("%'");
          results = doQuery(safequery,stmt,cb,offset,limit,results);
        }

        // see if the first one is exact
        if (results.size() > 0) {
          JavadocItem item = results.getFirst();
          if (item.getName().equals(query) || item.getFullName().equals(query)) {
            item.setExact(true);
          }
        }
        stmt.close();
      } finally {
        if (conn != null)
          conn.close();
      }
    } 
    catch (SQLException ex) {
      if (log.isLoggable(Level.CONFIG))
        log.log(Level.CONFIG,L.l("error with query `{0}': {1}",query,ex.getMessage()));
      throw ex;
    }

    return results;
  }

  private String escapeSQL(String q)
  {
    CharBuffer cb = CharBuffer.allocate();

    for (int i = 0; i < q.length(); i++) {
      char c = q.charAt(i);
      switch (c) {
        case '\'':
          cb.append("\\'");
          break;
        case '\"':
          cb.append("\\\"");
          break;
        case '*':
          cb.append("%");
          break;

        case '%':
          cb.append('\\');
        default:
          cb.append(c);
      }
    }

    return cb.close();
  }

  private LinkedList<JavadocItem> doQuery(String query, Statement stmt, CharBuffer cb, int offset, int limit, LinkedList<JavadocItem> results)
    throws SQLException
  {
    if (limit <= 0) {
      return results;
    }

    cb.append(" ORDER BY if(item.name = '");
    cb.append(query);
    cb.append("' OR item.fullname = '");
    cb.append(query);
    cb.append("',0,1) ");
    cb.append(",item.typ");
    cb.append(", LENGTH(item.name)");
    cb.append(" LIMIT ");
    cb.append(limit);
    if (offset > 0) {
      cb.append(" OFFSET ");
      cb.append(offset);
    }


    String q = cb.close();
    log.finest(L.l("query is [{0}]",q));

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

      while (rs.next()) {
        results.add(new JavadocItem(
              rs.getString(1),                     // name
              rs.getString(2),                     // fullname
              rs.getInt(3),                        // type
              rs.getString(4),                     // anchor
              rs.getString(5),                      // description
              new JavadocFile(_api.get(rs.getString(6)), // api
                               rs.getString(7))          // path
              ));
      }
    } finally {
      rs.close();
    }

    return results;

  }

}

