/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.exoplatform.services.jcr.impl.core.query.lucene;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.index.TermEnum;
import org.apache.lucene.search.FilteredTermEnum;
import org.apache.lucene.search.MultiTermQuery;
import org.apache.lucene.util.ToStringUtils;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * <code>CaseTermQuery</code> implements a term query which convert the term
 * from the index either to upper or lower case before it is matched.
 */
abstract class CaseTermQuery extends MultiTermQuery implements TransformConstants
{

   /**
    * The serial version UID
    */
   private static final long serialVersionUID = -4178467977221437090L;

   private static final Log LOG = ExoLogger.getLogger("exo.jcr.component.core.CaseTermQuery");

   /**
    * Indicates whether terms from the index should be lower-cased or
    * upper-cased.
    */
   protected final int transform;

   private final Term term;

   CaseTermQuery(Term term, int transform)
   {
      this.term = term;
      this.transform = transform;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   protected FilteredTermEnum getEnum(IndexReader reader) throws IOException
   {
      return new CaseTermEnum(reader);
   }

   /** Prints a user-readable version of this query. */
   @Override
   public String toString(String field)
   {
      StringBuilder buffer = new StringBuilder();
      if (!term.field().equals(field))
      {
         buffer.append(term.field());
         buffer.append(':');
      }
      buffer.append(term.text());
      buffer.append(ToStringUtils.boost(getBoost()));
      return buffer.toString();
   }

   static final class Upper extends CaseTermQuery
   {

      private static final long serialVersionUID = 4125411301640753714L;

      Upper(Term term)
      {
         super(term, TRANSFORM_UPPER_CASE);
      }
   }

   static final class Lower extends CaseTermQuery
   {

      private static final long serialVersionUID = -2496607808944458354L;

      Lower(Term term)
      {
         super(term, TRANSFORM_LOWER_CASE);
      }
   }

   private final class CaseTermEnum extends FilteredTermEnum
   {

      CaseTermEnum(IndexReader reader) throws IOException
      {
         // gather all terms that match
         // keep them in order and remember the doc frequency as value
         final Map<Term, Integer> orderedTerms = new LinkedHashMap<Term, Integer>();

         // there are always two range scans: one with an initial
         // lower case character and another one with an initial upper case
         // character
         List<RangeScan> rangeScans = new ArrayList<RangeScan>(2);
         int nameLength = FieldNames.getNameLength(term.text());
         String propName = term.text().substring(0, nameLength);
         OffsetCharSequence termText = new OffsetCharSequence(nameLength, term.text());
         OffsetCharSequence currentTerm = new OffsetCharSequence(nameLength, term.text(), transform);

         try
         {
            // start with a term using the lower case character for the first
            // character of the value.
            if (term.text().length() > nameLength)
            {
               // start with initial lower case
               StringBuilder lowerLimit = new StringBuilder(propName);
               String termStr = termText.toString();
               String upperTermStr = termStr.toUpperCase();
               String lowerTermStr = termStr.toLowerCase();

               lowerLimit.append(upperTermStr);
               lowerLimit.setCharAt(nameLength, Character.toLowerCase(lowerLimit.charAt(nameLength)));
               StringBuilder upperLimit = new StringBuilder(propName);
               upperLimit.append(lowerTermStr);
               rangeScans.add(new RangeScan(reader, new Term(term.field(), lowerLimit.toString()), new Term(term
                  .field(), upperLimit.toString())));

               // second scan with upper case start
               lowerLimit = new StringBuilder(propName);
               lowerLimit.append(upperTermStr);
               upperLimit = new StringBuilder(propName);
               upperLimit.append(lowerTermStr);
               upperLimit.setCharAt(nameLength, Character.toUpperCase(upperLimit.charAt(nameLength)));
               rangeScans.add(new RangeScan(reader, new Term(term.field(), lowerLimit.toString()), new Term(term
                  .field(), upperLimit.toString())));

            }
            else
            {
               // use term as is
               rangeScans.add(new RangeScan(reader, term, term));
            }

            for (TermEnum terms : rangeScans)
            {
               do
               {
                  Term t = terms.term();
                  if (t != null)
                  {
                     currentTerm.setBase(t.text());
                     int compare = currentTerm.compareTo(termText);
                     if (compare == 0)
                     {
                        orderedTerms.put(t, terms.docFreq());
                     }
                     else if (compare < 0)
                     {
                        // try next one
                     }
                     else
                     {
                        // compare > 0
                     }
                  }
                  else
                  {
                     break;
                  }
               }
               while (terms.next());
            }
         }
         finally
         {
            for (TermEnum terms : rangeScans)
            {
               try
               {
                  terms.close();
               }
               catch (IOException e)
               {
                  if (LOG.isTraceEnabled())
                  {
                     LOG.trace("An exception occurred: " + e.getMessage());
                  }
               }
            }
         }

         final Iterator<Term> it = orderedTerms.keySet().iterator();

         setEnum(new TermEnum()
         {

            private Term current;

            {
               getNext();
            }

            @Override
            public boolean next()
            {
               getNext();
               return current != null;
            }

            @Override
            public Term term()
            {
               return current;
            }

            @Override
            public int docFreq()
            {
               Integer docFreq = orderedTerms.get(current);
               return docFreq != null ? docFreq : 0;
            }

            @Override
            public void close()
            {
               // nothing to close
            }

            private void getNext()
            {
               current = it.hasNext() ? it.next() : null;
            }
         });
      }

      @Override
      protected boolean termCompare(Term term)
      {
         // they all match
         return true;
      }

      @Override
      public float difference()
      {
         return 1.0f;
      }

      @Override
      protected boolean endEnum()
      {
         // todo correct?
         return false;
      }
   }
}
