/*
 * Copyright (C) 2010 eXo Platform SAS.
 * 
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This software 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. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.xcmis.search.lucene;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.NotImplementedException;
import org.apache.commons.lang.Validate;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.BooleanClause.Occur;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.FieldDoc;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TopFieldDocs;
import org.xcmis.search.VisitException;
import org.xcmis.search.Visitors;
import org.xcmis.search.config.IndexConfiguration;
import org.xcmis.search.config.IndexConfigurationException;
import org.xcmis.search.config.SearchServiceConfiguration;
import org.xcmis.search.content.ContentEntry;
import org.xcmis.search.content.command.InvocationContext;
import org.xcmis.search.content.command.index.ModifyIndexCommand;
import org.xcmis.search.content.command.query.ExecuteSelectorCommand;
import org.xcmis.search.content.interceptors.QueryableIndexStorage;
import org.xcmis.search.lucene.content.VirtualTableResolver;
import org.xcmis.search.lucene.index.FieldNames;
import org.xcmis.search.lucene.index.IndexException;
import org.xcmis.search.lucene.index.IndexTransactionException;
import org.xcmis.search.lucene.index.LuceneIndexTransaction;
import org.xcmis.search.lucene.index.LuceneIndexer;
import org.xcmis.search.lucene.search.UUIDFieldSelector;
import org.xcmis.search.model.Limit;
import org.xcmis.search.model.constraint.Constraint;
import org.xcmis.search.model.operand.FullTextSearchScore;
import org.xcmis.search.model.operand.Length;
import org.xcmis.search.model.operand.LowerCase;
import org.xcmis.search.model.operand.NodeDepth;
import org.xcmis.search.model.operand.NodeLocalName;
import org.xcmis.search.model.operand.NodeName;
import org.xcmis.search.model.operand.PropertyValue;
import org.xcmis.search.model.operand.UpperCase;
import org.xcmis.search.model.ordering.Order;
import org.xcmis.search.model.ordering.Ordering;
import org.xcmis.search.result.ScoredRow;
import org.xcmis.search.value.NameConverter;
import org.xcmis.search.value.PathSplitter;
import org.xcmis.spi.utils.Logger;

/**
 * Base implementation of Lucene based {@link QueryableIndexStorage}.
 * 
 */
public abstract class AbstractLuceneQueryableIndexStorage extends QueryableIndexStorage
{

   /**
    * Class logger.
    */
   private static final Logger LOG = Logger.getLogger(AbstractLuceneQueryableIndexStorage.class);

   /**
    * The upper limit for the initial fetch size.
    */
   protected static final int MAX_FETCH_SIZE = 32 * 1024;

   /**
    * Convert one Sting name to other String name.
    */
   protected final NameConverter nameConverter;

   /**
    * Split path string to names.
    */
   protected final PathSplitter pathSplitter;

   /**
    * Reselve selector names to lucene querys.
    */
   protected final VirtualTableResolver tableResolver;

   /**
    * Node indexer.
    */
   protected LuceneIndexer nodeIndexer;

   /**
    * Index configuration.
    */
   protected IndexConfiguration indexConfuguration;

   /**
    * @param serviceConfiguration
    * @throws IndexException
    * @throws IndexConfigurationException
    * @throws org.xcmis.search.lucene.index.IndexException
    */
   public AbstractLuceneQueryableIndexStorage(SearchServiceConfiguration serviceConfiguration) throws IndexException
   {
      super(serviceConfiguration);
      Validate.notNull(serviceConfiguration.getTableResolver(),
         "The TableResolver may not be null in SearchServiceConfiguration");
      Validate.notNull(serviceConfiguration.getNameConverter(),
         "The NameConverter may not be null in SearchServiceConfiguration");
      Validate.notNull(serviceConfiguration.getPathSplitter(),
         "The PathSplitter may not be null in SearchServiceConfiguration");
      Validate.notNull(serviceConfiguration.getIndexConfuguration(),
         "The TableResolver  argument may not be null in SearchServiceConfiguration");
      Validate.notNull(serviceConfiguration.getIndexConfuguration().getRootParentUuid(),
         "The RootParentUuid  argument may not be null in IndexConfiguration");
      Validate.notNull(serviceConfiguration.getIndexConfuguration().getRootUuid(),
         "The RootUuid may not be null in IndexConfiguration");
      Validate.notNull(serviceConfiguration.getIndexConfuguration().getTikaConfiguration(),
         "The TikaConfiguration may not be null in IndexConfiguration");

      this.tableResolver = serviceConfiguration.getTableResolver();
      this.nameConverter = serviceConfiguration.getNameConverter();
      this.pathSplitter = serviceConfiguration.getPathSplitter();
      this.indexConfuguration = serviceConfiguration.getIndexConfuguration();

      this.nodeIndexer = new LuceneIndexer(indexConfuguration);

   }

   public Query getConstrainQuery(Constraint constraint, Map<String, Object> bindVariablesValues)
      throws VisitException, IndexException
   {
      LuceneQueryBuilder luceneQueryBuilder =
         new LuceneQueryBuilder(getIndexReader(), nameConverter, pathSplitter, bindVariablesValues, indexConfuguration);
      Visitors.visit(constraint, luceneQueryBuilder);
      return luceneQueryBuilder.getQuery();
   }

   /**
    * @see org.xcmis.search.content.interceptors.QueryableIndexStorage#visitExecuteSelectorCommand(org.xcmis.search.content.command.InvocationContext,
    *      org.xcmis.search.content.command.query.ExecuteSelectorCommand)
    */
   @Override
   public Object visitExecuteSelectorCommand(InvocationContext ctx, ExecuteSelectorCommand command) throws Throwable
   {

      List<ScoredRow> resultNodes = new ArrayList<ScoredRow>();
      Query query = (Query)ctx.getTableResolver().resolve(command.getSelector().getName(), true);
      if (command.getConstrains().size() > 0)
      {
         BooleanQuery booleanQuery = new BooleanQuery();

         for (Constraint constrain : command.getConstrains())
         {
            booleanQuery.add(getConstrainQuery(constrain, command.getBindVariablesValues()), Occur.MUST);
         }

         booleanQuery.add(query, Occur.MUST);
         query = booleanQuery;
      }
      // Open writer

      IndexSearcher searcher = null;
      try
      {
         // get result
         IndexReader indexReader = getIndexReader();
         if (indexReader != null)
         {
            searcher = new Searcher(indexReader);

            // query
            Limit limit = command.getLimit();
            int hits = Math.min(MAX_FETCH_SIZE, limit.getOffset() + limit.getRowLimit());
            TopFieldDocs topDocs = searcher.search(query, null, hits, getSort(command.getOrderings()));

            resultNodes = new LinkedList<ScoredRow>();
            for (int i = limit.getOffset(); i < topDocs.scoreDocs.length; i++)
            {
               // get identifiers
               final Document doc = searcher.doc(topDocs.scoreDocs[i].doc, new UUIDFieldSelector());
               final String id = doc.get(FieldNames.UUID);
               final FieldDoc fieldDoc = (FieldDoc) topDocs.scoreDocs[i];
               Float score = Float.NaN;
               if(fieldDoc.fields[0] instanceof Float)
               {
                   score = (Float) fieldDoc.fields[0];
               }
               resultNodes.add(new ScoredRow(command.getAlias().getName(), id, score));
            }
         }
      }
      catch (final CorruptIndexException e)
      {
         throw new IndexException(e.getLocalizedMessage(), e);
      }
      catch (final IOException e)
      {
         throw new IndexException(e.getLocalizedMessage(), e);
      }
      finally
      {
         try
         {
            if (searcher != null)
            {
               searcher.close();
            }
         }
         catch (final IOException e)
         {
            throw new IndexException(e.getLocalizedMessage(), e);
         }
      }
      return resultNodes;
   }

   /**
    * @see org.xcmis.search.content.interceptors.QueryableIndexStorage#visitModifyIndexCommand(org.xcmis.search.content.command.InvocationContext,
    *      org.xcmis.search.content.command.index.ModifyIndexCommand)
    */
   @Override
   public Object visitModifyIndexCommand(InvocationContext ctx, ModifyIndexCommand command) throws Throwable
   {
      Map<String, Document> addedDocuments = new HashMap<String, Document>();
      // indexing content
      for (ContentEntry entry : command.getAddedDocuments())
      {
         addedDocuments.put(entry.getIdentifier(), nodeIndexer.createDocument(entry));
      }

      LuceneIndexTransaction indexTransaction =
         new LuceneIndexTransaction(addedDocuments, command.getDeletedDocuments());

      return save(indexTransaction);
   }

   /**
    * Different lucene storage's should override this method.
    */
   protected abstract IndexReader getIndexReader() throws IndexException;

   /**
    * @param indexTransaction
    * @return
    * @throws IndexTransactionException
    * @throws IndexException
    */
   protected abstract Object save(LuceneIndexTransaction indexTransaction) throws IndexException,
      IndexTransactionException;

   /**
    * Return lucene sorter by list of orderings.
    * 
    * @param list
    * @return
    * @throws VisitException
    */
   private Sort getSort(List<Ordering> list) throws VisitException
   {
      if (list.size() > 0)
      {
         SortField[] fields = new SortField[list.size()];
         SortFieldVisitor sortVisitor = new SortFieldVisitor();
         int i = 0;
         for (Ordering ordering : list)
         {
            Visitors.visitAll(ordering, sortVisitor);
            fields[i++] = sortVisitor.getSortField();
         }
         return new Sort(fields);

      }
      return new Sort();
   }

   private class SortFieldVisitor extends Visitors.AbstractModelVisitor
   {

      private Order order;

      private SortField sortField;

      public SortField getSortField()
      {
         return sortField;
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.FullTextSearchScore)
       */
      @Override
      public void visit(FullTextSearchScore node) throws VisitException
      {
         //By default result sorted score descendant
         sortField = new SortField(null, SortField.SCORE, order == Order.ASCENDING);
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.Length)
       */
      @Override
      public void visit(Length node) throws VisitException
      {
         throw new NotImplementedException();
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.LowerCase)
       */
      @Override
      public void visit(LowerCase node) throws VisitException
      {
         throw new NotImplementedException();
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeDepth)
       */
      @Override
      public void visit(NodeDepth depth) throws VisitException
      {
         throw new NotImplementedException();
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeLocalName)
       */
      @Override
      public void visit(NodeLocalName node) throws VisitException
      {
         super.visit(node);
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.NodeName)
       */
      @Override
      public void visit(NodeName node) throws VisitException
      {
         throw new NotImplementedException();
      }

      /**
       * 
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.ordering.Ordering)
       */
      public void visit(Ordering node) throws VisitException
      {
         order = node.getOrder();
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.PropertyValue)
       */
      @Override
      public void visit(PropertyValue node) throws VisitException
      {
        //AUTO mode was removed since version 3.0. Therefore we will used STRING type as sort type.
         sortField =
            new SortField(FieldNames.createPropertyFieldName(node.getPropertyName()), 
                    SortField.STRING, order == Order.DESCENDING);
      }

      /**
       * @see org.xcmis.search.Visitors.AbstractModelVisitor#visit(org.xcmis.search.model.operand.UpperCase)
       */
      @Override
      public void visit(UpperCase node) throws VisitException
      {
         throw new NotImplementedException();
      }
   }
}
