/*
 * 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.exoplatform.services.jcr.impl.core.query.lucene;

import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.document.AbstractField;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Fieldable;
import org.apache.lucene.index.FieldInfo.IndexOptions;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

/**
 * This class is used to serialize and deserialize the changes to apply to the lucene index.
 * 
 * @author <a href="mailto:nicolas.filotto@exoplatform.com">Nicolas Filotto</a>
 * @version $Id$
 */
public class ChangesHolder implements Externalizable
{

   private static final int STORED_FLAG = 1;

   private static final int INDEXED_FLAG = 1 << 2;

   private static final int TOKENIZED_FLAG = 1 << 3;

   private static final int OMIT_NORMS_FLAG = 1 << 4;

   private static final int BINARY_FLAG = 1 << 5;

   private static final int STORE_TERM_VECTOR_FLAG = 1 << 6;

   private static final int STORE_POSITION_WITH_TERM_VECTOR_FLAG = 1 << 7;

   private static final int STORE_OFFSET_WITH_TERM_VECTOR_FLAG = 1 << 8;

   private static final int LAZY_FLAG = 1 << 9;

   private static final int OMIT_TF_FLAG = 1 << 10;

   private static final int BOOST_FLAG = 1 << 11;

   /**
    * List of doc ids to remove from the index
    */
   private List<String> remove;

   /**
    * Collection of Lucene Documents to add to the index
    */
   private Collection<Document> add;

   /**
    * Default constructor used during the deserializing phase
    */
   public ChangesHolder()
   {
   }

   /**
    * @param remove Collection of doc ids to remove from the index
    * @param add Lucene Documents to add to the index
    */
   public ChangesHolder(Collection<String> remove, Collection<Document> add)
   {
      this.remove = new ArrayList<String>(remove);
      this.add = add;
   }

   /**
    * @return the collection of doc id to remove
    */
   public Collection<String> getRemove()
   {
      return remove;
   }

   /**
    * @return the collection of lucene document to add
    */
   public Collection<Document> getAdd()
   {
      return add;
   }

   /**
    * @return the collection of id of lucene document to add
    */
   public Collection<String> getAddIds()
   {
      Collection<String> ids = new LinkedList<String>();
      for (Document doc : add)
      {
         ids.add(getDocId(doc));
      }
      return ids;
   }

   /**
    * @return the id of the given lucene doc
    */
   public String getDocId(Document doc)
   {
      return doc.get(FieldNames.UUID);
   }

   /**
    * {@inheritDoc}
    */
   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException
   {
      int length = in.readInt();
      this.remove = new ArrayList<String>(length);
      for (int i = 0; i < length; i++)
      {
         remove.add(in.readUTF());
      }
      this.add = new LinkedList<Document>();
      while (in.readBoolean())
      {
         Document doc = new Document();
         doc.setBoost(in.readFloat());
         int l = in.readInt();
         for (int i = 0; i < l; i++)
         {
            doc.add(readField(in, doc));
         }
         add.add(doc);
      }
   }

   /**
    * Deserialize the field from the given {@link ObjectInput}
    * @param in the stream from which we deserialize the Field
    * @return the deserialized field
    * @throws IOException 
    * @throws ClassNotFoundException 
    */
   private static Field readField(ObjectInput in, Document doc) throws IOException, ClassNotFoundException
   {
      String name = in.readUTF();
      int flags = in.readInt();
      float boost = (flags & BOOST_FLAG) > 0 ? in.readFloat() : 1.0f;
      Object value = in.readObject();
      Field field;
      if (value instanceof TokenStream)
      {
         field = new Field(name, (TokenStream)value, getTermVectorParameter(flags));
      }
      else
      {
         // The value is a String
         field =
            new Field(name, (String)value, getStoreParameter(flags), getIndexParameter(flags),
               getTermVectorParameter(flags));
      }
      field.setBoost(boost);
      field.setOmitNorms((flags & OMIT_NORMS_FLAG) > 0);
      IndexOptions indexOptions;
      if ((flags & OMIT_TF_FLAG) > 0) {
         indexOptions = IndexOptions.DOCS_ONLY;
       } else {
         indexOptions = IndexOptions.DOCS_AND_FREQS_AND_POSITIONS;
       }
      field.setIndexOptions(indexOptions);
      return field;
   }

   /**
    * Returns the index parameter extracted from the flags.
    *
    * @param flags the flags of the Lucene field.
    * @return the index parameter corresponding to the given flags.
    */
   private static Field.Index getIndexParameter(int flags)
   {
      if ((flags & INDEXED_FLAG) == 0)
      {
         return Field.Index.NO;
      }
      else if ((flags & TOKENIZED_FLAG) > 0)
      {
         return Field.Index.ANALYZED;
      }
      else
      {
         return Field.Index.NOT_ANALYZED;
      }
   }

   /**
    * Returns the store parameter extracted from the flags.
    *
    * @param flags the flags of the Lucene field.
    * @return the store parameter corresponding to the given flags.
    */
   private static Field.Store getStoreParameter(int flags)
   {
      if ((flags & STORED_FLAG) > 0)
      {
         return Field.Store.YES;
      }
      else
      {
         return Field.Store.NO;
      }
   }

   /**
    * Returns the term vector parameter extracted from the flags.
    *
    * @param flags the flags of the Lucene field.
    * @return the term vector parameter corresponding to the given flags.
    */
   private static Field.TermVector getTermVectorParameter(int flags)
   {
      if (((flags & STORE_POSITION_WITH_TERM_VECTOR_FLAG) > 0) && ((flags & STORE_OFFSET_WITH_TERM_VECTOR_FLAG) > 0))
      {
         return Field.TermVector.WITH_POSITIONS_OFFSETS;
      }
      else if ((flags & STORE_POSITION_WITH_TERM_VECTOR_FLAG) > 0)
      {
         return Field.TermVector.WITH_POSITIONS;
      }
      else if ((flags & STORE_OFFSET_WITH_TERM_VECTOR_FLAG) > 0)
      {
         return Field.TermVector.WITH_OFFSETS;
      }
      else if ((flags & STORE_TERM_VECTOR_FLAG) > 0)
      {
         return Field.TermVector.YES;
      }
      else
      {
         return Field.TermVector.NO;
      }
   }

   /**
    * {@inheritDoc}
    */
   public void writeExternal(ObjectOutput out) throws IOException
   {
      int length = remove.size();
      out.writeInt(length);
      for (int i = 0; i < length; i++)
      {
         out.writeUTF(remove.get(i));
      }
      out.flush();
      for (Document doc : add)
      {
         // Indicate that there is a doc to come
         out.writeBoolean(true);
         // boost
         out.writeFloat(doc.getBoost());
         List<Fieldable> fields = doc.getFields();
         int l = fields.size();
         out.writeInt(l);
         for (int i = 0; i < l; i++)
         {
            writeField(out, fields.get(i));
         }
         out.flush();
      }
      // There is no doc anymore
      out.writeBoolean(false);
   }

   /**
    * Serialize the Field into the given {@link ObjectOutput}
    * @param out the stream in which we serialize the Field
    * @param field the Field instance to serialize
    * @throws IOException if the Field could not be serialized
    */
   private static void writeField(ObjectOutput out, Fieldable field) throws IOException
   {
      // Name
      out.writeUTF(field.name());
      // Flags
      writeFlags(out, field);
      if (field.getBoost() != 1.0f)
      {
         // Boost
         out.writeFloat(field.getBoost());
      }
      // Value
      writeValue(out, field);
   }

   /**
    * Serialize the value into the given {@link ObjectOutput}
    * @param out the stream in which we serialize the value
    * @param field the field from which we extract the value
    * @throws IOException if the value could not be serialized
    */
   private static void writeValue(ObjectOutput out, Fieldable field) throws IOException
   {
      Object o = field.stringValue();
      if (o != null)
      {
         // Use writeObject instead of writeUTF because the value could contain unsupported
         // characters
         out.writeObject(o);
         return;
      }
      o = field.tokenStreamValue();
      if (o != null)
      {
         out.writeObject(o);
         return;
      }
      o = field.readerValue();
      throw new RuntimeException("Unsupported value " + o);
   }

   /**
    * Serialize the flags into the given {@link ObjectOutput}
    * @param out the stream in which we serialize the flags
    * @param field the field from which we extract the flags
    * @throws IOException if the flags could not be serialized
    */
   private static void writeFlags(ObjectOutput out, Fieldable field) throws IOException
   {
      int flags = 0;
      if (field.isStored())
      {
         flags |= STORED_FLAG;
      }
      if (field.isIndexed())
      {
         flags |= INDEXED_FLAG;
      }
      if (field.isTokenized())
      {
         flags |= TOKENIZED_FLAG;
      }
      if (field.getOmitNorms())
      {
         flags |= OMIT_NORMS_FLAG;
      }
      if (field.isBinary())
      {
         flags |= BINARY_FLAG;
      }
      if (field.isTermVectorStored())
      {
         flags |= STORE_TERM_VECTOR_FLAG;
      }
      if (field.isStorePositionWithTermVector())
      {
         flags |= STORE_POSITION_WITH_TERM_VECTOR_FLAG;
      }
      if (field.isStoreOffsetWithTermVector())
      {
         flags |= STORE_OFFSET_WITH_TERM_VECTOR_FLAG;
      }
      if (field.isLazy())
      {
         flags |= LAZY_FLAG;
      }
      if (field instanceof AbstractField && ((AbstractField)field).getIndexOptions() == IndexOptions.DOCS_ONLY)
      {
         flags |= OMIT_TF_FLAG;
      }
      if (field.getBoost() != 1.0f)
      {
         flags |= BOOST_FLAG;
      }
      out.writeInt(flags);
   }
}
