/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2003 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.index;

import java.io.*;
import java.lang.ref.*;
import java.util.*;
import org.apache.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import com.silverwrist.util.*;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;
import com.silverwrist.dynamo.util.*;

class IndexServiceImpl implements IndexService
{
  /*--------------------------------------------------------------------------------
   * Internal counting HitCollector
   *--------------------------------------------------------------------------------
   */

  private static class CountingCollector extends HitCollector
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private int m_count = 0;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    CountingCollector()
    { // do nothing
    } // end constructor

    /*====================================================================
     * Abstract implementations from class HitCollector
     *====================================================================
     */

    public void collect(int doc, float score)
    {
      m_count++;

    } // end collect

    /*====================================================================
     * External operations
     *====================================================================
     */

    int getCount()
    {
      return m_count;

    } // end getCount

  } // end class CountingCollector

  /*--------------------------------------------------------------------------------
   * Internal HitCollector that gathers a request subset
   *--------------------------------------------------------------------------------
   */

  private class SubsetCollector extends HitCollector
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private int[] m_docs;
    private float[] m_scores;
    private int m_offset;
    private int m_size = 0;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    SubsetCollector(int offset, int count)
    {
      m_docs = new int[count];
      m_scores = new float[count];
      m_offset = offset;

    } // end constructor

    /*====================================================================
     * Abstract implementations from class HitCollector
     *====================================================================
     */

    public void collect(int doc, float score)
    {
      if (m_offset>0)
      { // skip documents at beginning of list
	m_offset--;
	return;

      } // end if

      if (m_size<m_docs.length)
      { // add document index and score to the list
	m_docs[m_size] = doc;
	m_scores[m_size++] = score;

      } // end if

    } // end collect

    /*====================================================================
     * External operations
     *====================================================================
     */

    public List outputItems(IndexReader irdr) throws IOException, IndexException
    {
      if (m_size==0)
	return Collections.EMPTY_LIST;
      ArrayList rc = new ArrayList(m_size);
      for (int i=0; i<m_size; i++)
      { // get the document and retrieve its "id" field, then use that to get the object
	Document doc = irdr.document(m_docs[i]);
	Field id_field = doc.getField("id");
	String fulltag = id_field.stringValue();
	if (fulltag==null)
	  fulltag = IOUtils.load(id_field.readerValue()).toString();
	String[] elts = StringUtils.split1(fulltag,'|',3);
	Object value = m_base.resolveObjectReference(elts[0],elts[1],elts[2]);
	rc.add(new ItemAndScore(value,m_scores[i]));

      } // end for

      return Collections.unmodifiableList(rc);

    } // end outputItems

  } // end class SubsetCollector

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static Logger logger = Logger.getLogger(IndexServiceImpl.class);

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private QualifiedNameKey m_identity;
  private IndexManagerObject m_base;
  private Analyzer m_analyzer;
  private IndexDirectoryImpl m_directory;
  private DirectoryAutoCleanup m_cleanup;

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  IndexServiceImpl(IndexManagerObject base, QualifiedNameKey identity, int ndx, IndexOps ops, ReferenceQueue rq,
		   boolean create) throws DatabaseException, IndexException
  {
    m_analyzer = createAnalyzer(ops.getAnalyzerClassName(ndx));
    m_base = base;
    m_identity = identity;
    m_directory = new IndexDirectoryImpl(ndx,ops);
    m_cleanup = new DirectoryAutoCleanup(this,m_directory,rq);
    if (create)
    { // create the new index
      try
      { // Use an IndexWriter to create the index for the first time.
	IndexWriter iwr = new IndexWriter(m_directory,m_analyzer,true);
	iwr.close();

      } // end try
      catch (IOException e)
      { // translate Lucene's IOException here
	IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","indexCreate.fail",e);
	ie.setParameter(0,m_identity.toString());
	throw ie;

      } // end catch

    } // end if

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private static final void visitAllDocuments(IndexSearcher srch, HitCollector c) throws IOException
  {
    for (int i=0; i<srch.maxDoc(); i++)
      c.collect(i,1.0F);

  } // end visitAllDocuments

  private final String createTag(String namespace, String name, Object obj) throws IndexException
  {
    String objtag = m_base.getResolver(namespace,name).getResolverTag(obj);
    return namespace + "|" + name + "|" + objtag;

  } // end createTag

  private final Query compileInternal(String query_string, java.util.Date date_low, java.util.Date date_high,
				      DynamoUser match_owner, String match_scope) throws IndexException
  {
    ArrayList queries = new ArrayList();
    if (query_string!=null)
    { // we have a query language string...
      try
      { // parse the query string, which matches on the "text" field only
	queries.add(Parser.parse(query_string));

      } // end try
      catch (ParseException pe)
      { // parse error in the query
	IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","query.syntax",pe);
	ie.setParameter(0,pe.getMessage());
	throw ie;

      } // end catch

    } // end if

    if ((date_low!=null) || (date_high!=null))
    { // add an inclusive range of dates
      Term term_low = null, term_high = null;
      if (date_low!=null)
	term_low = new Term("date",DateField.dateToString(date_low));
      if (date_high!=null)
	term_high = new Term("date",DateField.dateToString(date_high));
      queries.add(new RangeQuery(term_low,term_high,true));

    } // end if

    if (match_owner!=null)
      queries.add(new TermQuery(new Term("owner",match_owner.getName())));
    if (match_scope!=null)
    { // treat "scope" as a possible wildcard match and create it
      if (match_scope.indexOf('?')>=0)
	queries.add(new WildcardQuery(new Term("scope",match_scope)));
      else if (match_scope.indexOf('*')>=0)
      { // append another query
	String s = match_scope.substring(0,match_scope.length()-1);
	if (s.indexOf('*')<0)
	  queries.add(new PrefixQuery(new Term("scope",s)));
	else
	  queries.add(new WildcardQuery(new Term("scope",match_scope)));

      } // end else if
      else  // match the scope directly
	queries.add(new TermQuery(new Term("scope",match_scope)));

    } // end if

    // Boil down all the queries for me.
    if (queries.size()==0)
      return null;
    if (queries.size()==1)
      return (Query)(queries.get(0));
    BooleanQuery rc = new BooleanQuery();
    for (int i=0; i<queries.size(); i++)
      rc.add((Query)(queries.get(i)),true,false);
    return rc;

  } // end compileInternal

  private final List doQuery(Query query, int offset, int count) throws IndexException
  {
    SubsetCollector subc = new SubsetCollector(offset,count);
    List rc = null;
    IndexReader irdr = null;
    IndexSearcher srch = null;
    try
    { // run that puppy!
      irdr = IndexReader.open(m_directory);
      srch = new IndexSearcher(irdr);
      if (query==null)
	visitAllDocuments(srch,subc);
      else
	srch.search(query,subc);
      rc = subc.outputItems(irdr);

    } // end try
    catch (IOException e)
    { // the query failed somehow - throw an error
      throw new IndexException(IndexServiceImpl.class,"IndexMessages","query.fail",e);

    } // end catch
    finally
    { // make sure we close down OK
      try
      { // close the search and index reader
	if (srch!=null)
	  srch.close();
	if (irdr!=null)
	  irdr.close();

      } // end try
      catch (IOException e)
      { // shouldn't happen
	logger.warn("query(): error closing stuff",e);

      } // end catch

    } // end finally

    return rc;

  } // end doQuery

  private final int doQueryCount(Query query) throws IndexException
  {
    CountingCollector cc = new CountingCollector();
    IndexSearcher srch = null;
    try
    { // run that puppy!
      srch = new IndexSearcher(m_directory);
      if (query==null)
	visitAllDocuments(srch,cc);
      else
	srch.search(query,cc);

    } // end try
    catch (IOException e)
    { // the query failed somehow - throw an error
      throw new IndexException(IndexServiceImpl.class,"IndexMessages","query.fail",e);

    } // end catch
    finally
    { // make sure we close down OK
      try
      { // close the search and index reader
	if (srch!=null)
	  srch.close();

      } // end try
      catch (IOException e)
      { // shouldn't happen
	logger.warn("queryCount(): error closing stuff",e);

      } // end catch

    } // end finally

    return cc.getCount();

  } // end doQueryCount

  /*--------------------------------------------------------------------------------
   * Implementations from interface IndexService
   *--------------------------------------------------------------------------------
   */

  public void addItem(String item_namespace, String item_name, Object item, String scope, java.util.Date date,
		      DynamoUser owner, String text) throws IndexException
  {
    // Create a new Lucene Document containing the item information.
    Document doc = new Document();
    doc.add(Field.Keyword("id",createTag(item_namespace,item_name,item)));
    doc.add(Field.Keyword("date",date));
    doc.add(Field.Keyword("owner",owner.getName()));
    doc.add(Field.Keyword("scope",scope));
    doc.add(Field.UnStored("text",text));

    try
    { // Use an IndexWriter to write it to the index.
      IndexWriter iwr = new IndexWriter(m_directory,m_analyzer,false);
      iwr.addDocument(doc);
      iwr.close();

    } // end try
    catch (IOException e)
    { // translate Lucene's IOException here
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","addItem.fail",e);
      ie.setParameter(0,item_namespace);
      ie.setParameter(1,item_name);
      ie.setParameter(2,m_identity.toString());
      throw ie;

    } // end catch

  } // end addItem

  public boolean deleteItem(String item_namespace, String item_name, Object item) throws IndexException
  {
    // Create a Lucene Term matching the given document.
    Term term = new Term("id",createTag(item_namespace,item_name,item));

    try
    { // Use an IndexReader to delete the appropriate document.
      IndexReader irdr = IndexReader.open(m_directory);
      int ndel = irdr.delete(term);
      irdr.close();
      return (ndel>0);

    } // end try
    catch (IOException e)
    { // translate Lucene's IOException here
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","deleteItem.fail",e);
      ie.setParameter(0,item_namespace);
      ie.setParameter(1,item_name);
      ie.setParameter(2,m_identity.toString());
      throw ie;

    } // end catch

  } // end deleteItem

  public CompiledQuery compileQuery(String query_string, java.util.Date date_low, java.util.Date date_high,
				    DynamoUser match_owner, String match_scope) throws IndexException
  {
    Query query = compileInternal(query_string,date_low,date_high,match_owner,match_scope);
    return new CompiledQuery(query);

  } // end compileQuery

  public List query(String query_string, java.util.Date date_low, java.util.Date date_high, DynamoUser match_owner,
                    String match_scope, int offset, int count) throws IndexException
  {
    Query query = compileInternal(query_string,date_low,date_high,match_owner,match_scope);
    return doQuery(query,offset,count);

  } // end query

  public List query(CompiledQuery query, int offset, int count) throws IndexException
  {
    return doQuery(query.getQuery(),offset,count);

  } // end query

  public int queryCount(String query_string, java.util.Date date_low, java.util.Date date_high, DynamoUser match_owner,
			String match_scope) throws IndexException
  {
    Query query = compileInternal(query_string,date_low,date_high,match_owner,match_scope);
    return doQueryCount(query);

  } // end queryCount

  public int queryCount(CompiledQuery query) throws IndexException
  {
    return doQueryCount(query.getQuery());

  } // end queryCount

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  void abandon()
  {
    m_cleanup.clear();
    m_directory.abandon();

  } // end abandon

  /*--------------------------------------------------------------------------------
   * External static operations
   *--------------------------------------------------------------------------------
   */

  static Analyzer createAnalyzer(String classname) throws IndexException
  {
    try
    { // Create the analyzer class.
      return (Analyzer)(Class.forName(classname).newInstance());

    } // end try
    catch (ClassNotFoundException e)
    { // unable to find the Analyzer class
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","analyzer.class.notfound",e);
      ie.setParameter(0,classname);
      throw ie;

    } // end catch
    catch (IllegalAccessException e)
    { // problem creating the analyzer class
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","analyzer.noCreate",e);
      ie.setParameter(0,classname);
      throw ie;

    } // end catch
    catch (InstantiationException e)
    { // unable to instantiate the class
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","analyzer.noCreate",e);
      ie.setParameter(0,classname);
      throw ie;

    } // end catch
    catch (ClassCastException e)
    { // bad class specified
      IndexException ie = new IndexException(IndexServiceImpl.class,"IndexMessages","analyzer.badType",e);
      ie.setParameter(0,classname);
      throw ie;

    } // end catch

  } // end createAnalyzer

} // end class IndexServiceImpl
