/*
 * 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.apache.lucene.search.vectorhighlight;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.lucene.search.vectorhighlight.FieldQuery.QueryPhraseMap;
import org.apache.lucene.search.vectorhighlight.FieldTermStack.TermInfo;
import org.apache.lucene.util.MergedIterator;

/**
 * FieldPhraseList has a list of WeightedPhraseInfo that is used by FragListBuilder
 * to create a FieldFragList object.
 */
public class FieldPhraseList {
  /**
   * List of non-overlapping WeightedPhraseInfo objects.
   */
  LinkedList<WeightedPhraseInfo> phraseList = new LinkedList<>();
  
  /**
   * create a FieldPhraseList that has no limit on the number of phrases to analyze
   * 
   * @param fieldTermStack FieldTermStack object
   * @param fieldQuery FieldQuery object
   */
  public FieldPhraseList( FieldTermStack fieldTermStack, FieldQuery fieldQuery){
      this (fieldTermStack, fieldQuery, Integer.MAX_VALUE);
  }
  
  /**
   * return the list of WeightedPhraseInfo.
   * 
   * @return phraseList.
   */ 
  public List<WeightedPhraseInfo> getPhraseList() {
    return phraseList; 
  }
  
  /**
   * a constructor.
   * 
   * @param fieldTermStack FieldTermStack object
   * @param fieldQuery FieldQuery object
   * @param phraseLimit maximum size of phraseList
   */
  public FieldPhraseList( FieldTermStack fieldTermStack, FieldQuery fieldQuery, int phraseLimit ){
    final String field = fieldTermStack.getFieldName();

    LinkedList<TermInfo> phraseCandidate = new LinkedList<>();
    QueryPhraseMap currMap = null;
    QueryPhraseMap nextMap = null;
    while( !fieldTermStack.isEmpty() && (phraseList.size() < phraseLimit) )
    {      
      phraseCandidate.clear();

      TermInfo ti = null;
      TermInfo first = null;
      
      first = ti = fieldTermStack.pop();
      currMap = fieldQuery.getFieldTermMap( field, ti.getText() );
      while (currMap == null && ti.getNext() != first) {
        ti = ti.getNext();
        currMap = fieldQuery.getFieldTermMap( field, ti.getText() );
      }

      // if not found, discard top TermInfo from stack, then try next element
      if( currMap == null ) continue;
      
      // if found, search the longest phrase
      phraseCandidate.add( ti );
      while( true ){
        first = ti = fieldTermStack.pop();
        nextMap = null;
        if( ti != null ) {
          nextMap = currMap.getTermMap( ti.getText() );
          while (nextMap == null && ti.getNext() != first) {
            ti = ti.getNext();
            nextMap = currMap.getTermMap( ti.getText() );
          }
        }
        if( ti == null || nextMap == null ){
          if( ti != null ) 
            fieldTermStack.push( ti );
          if( currMap.isValidTermOrPhrase( phraseCandidate ) ){
            addIfNoOverlap( new WeightedPhraseInfo( phraseCandidate, currMap.getBoost(), currMap.getTermOrPhraseNumber() ) );
          }
          else{
            while( phraseCandidate.size() > 1 ){
              fieldTermStack.push( phraseCandidate.removeLast() );
              currMap = fieldQuery.searchPhrase( field, phraseCandidate );
              if( currMap != null ){
                addIfNoOverlap( new WeightedPhraseInfo( phraseCandidate, currMap.getBoost(), currMap.getTermOrPhraseNumber() ) );
                break;
              }
            }
          }
          break;
        }
        else{
          phraseCandidate.add( ti );
          currMap = nextMap;
        }
      }
    }
  }

  /**
   * Merging constructor.
   *
   * @param toMerge FieldPhraseLists to merge to build this one
   */
  public FieldPhraseList( FieldPhraseList[] toMerge ) {
    // Merge all overlapping WeightedPhraseInfos
    // Step 1.  Sort by startOffset, endOffset, and boost, in that order.
    @SuppressWarnings( { "rawtypes", "unchecked" } )
    Iterator< WeightedPhraseInfo >[] allInfos = new Iterator[ toMerge.length ];
    int index = 0;
    for ( FieldPhraseList fplToMerge : toMerge ) {
      allInfos[ index++ ] = fplToMerge.phraseList.iterator();
    }
    MergedIterator< WeightedPhraseInfo > itr = new MergedIterator<>( false, allInfos );
    // Step 2.  Walk the sorted list merging infos that overlap
    phraseList = new LinkedList<>();
    if ( !itr.hasNext() ) {
      return;
    }
    List< WeightedPhraseInfo > work = new ArrayList<>();
    WeightedPhraseInfo first = itr.next();
    work.add( first );
    int workEndOffset = first.getEndOffset();
    while ( itr.hasNext() ) {
      WeightedPhraseInfo current = itr.next();
      if ( current.getStartOffset() <= workEndOffset ) {
        workEndOffset = Math.max( workEndOffset, current.getEndOffset() );
        work.add( current );
      } else {
        if ( work.size() == 1 ) {
          phraseList.add( work.get( 0 ) );
          work.set( 0, current );
        } else {
          phraseList.add( new WeightedPhraseInfo( work ) );
          work.clear();
          work.add( current );
        }
        workEndOffset = current.getEndOffset();
      }
    }
    if ( work.size() == 1 ) {
      phraseList.add( work.get( 0 ) );
    } else {
      phraseList.add( new WeightedPhraseInfo( work ) );
      work.clear();
    }
  }

  public void addIfNoOverlap( WeightedPhraseInfo wpi ){
    for( WeightedPhraseInfo existWpi : getPhraseList() ){
      if( existWpi.isOffsetOverlap( wpi ) ) {
        // WeightedPhraseInfo.addIfNoOverlap() dumps the second part of, for example, hyphenated words (social-economics). 
        // The result is that all informations in TermInfo are lost and not available for further operations. 
        existWpi.getTermsInfos().addAll( wpi.getTermsInfos() );
        return;
      }
    }
    getPhraseList().add( wpi );
  }
  
  /**
   * Represents the list of term offsets and boost for some text
   */
  public static class WeightedPhraseInfo implements Comparable< WeightedPhraseInfo > {
    private List<Toffs> termsOffsets;   // usually termsOffsets.size() == 1,
                            // but if position-gap > 1 and slop > 0 then size() could be greater than 1
    private float boost;  // query boost
    private int seqnum;
    
    private ArrayList<TermInfo> termsInfos;
    
    /**
     * Text of the match, calculated on the fly.  Use for debugging only.
     * @return the text
     */
    public String getText() {
      StringBuilder text = new StringBuilder();
      for ( TermInfo ti: termsInfos ) {
        text.append( ti.getText() );
      }
      return text.toString();
    }

    /**
     * @return the termsOffsets
     */
    public List<Toffs> getTermsOffsets() {
      return termsOffsets;
    }

    /**
     * @return the boost
     */
    public float getBoost() {
      return boost;
    }

    /**
     * @return the termInfos 
     */    
    public List<TermInfo> getTermsInfos() {
      return termsInfos;
    }

    public WeightedPhraseInfo( LinkedList<TermInfo> terms, float boost ){
      this( terms, boost, 0 );
    }
    
    public WeightedPhraseInfo( LinkedList<TermInfo> terms, float boost, int seqnum ){
      this.boost = boost;
      this.seqnum = seqnum;
      
      // We keep TermInfos for further operations
      termsInfos = new ArrayList<>( terms );
      
      termsOffsets = new ArrayList<>( terms.size() );
      TermInfo ti = terms.get( 0 );
      termsOffsets.add( new Toffs( ti.getStartOffset(), ti.getEndOffset() ) );
      if( terms.size() == 1 ){
        return;
      }
      int pos = ti.getPosition();
      for( int i = 1; i < terms.size(); i++ ){
        ti = terms.get( i );
        if( ti.getPosition() - pos == 1 ){
          Toffs to = termsOffsets.get( termsOffsets.size() - 1 );
          to.setEndOffset( ti.getEndOffset() );
        }
        else{
          termsOffsets.add( new Toffs( ti.getStartOffset(), ti.getEndOffset() ) );
        }
        pos = ti.getPosition();
      }
    }

    /**
     * Merging constructor.  Note that this just grabs seqnum from the first info.
     */
    public WeightedPhraseInfo( Collection< WeightedPhraseInfo > toMerge ) {
      // Pretty much the same idea as merging FieldPhraseLists:
      // Step 1.  Sort by startOffset, endOffset
      //          While we are here merge the boosts and termInfos
      Iterator< WeightedPhraseInfo > toMergeItr = toMerge.iterator();
      if ( !toMergeItr.hasNext() ) {
        throw new IllegalArgumentException( "toMerge must contain at least one WeightedPhraseInfo." );
      }
      WeightedPhraseInfo first = toMergeItr.next();
      @SuppressWarnings( { "rawtypes", "unchecked" } )
      Iterator< Toffs >[] allToffs = new Iterator[ toMerge.size() ];
      termsInfos = new ArrayList<>();
      seqnum = first.seqnum;
      boost = first.boost;
      allToffs[ 0 ] = first.termsOffsets.iterator();
      int index = 1;
      while ( toMergeItr.hasNext() ) {
        WeightedPhraseInfo info = toMergeItr.next();
        boost += info.boost;
        termsInfos.addAll( info.termsInfos );
        allToffs[ index++ ] = info.termsOffsets.iterator();
      }
      // Step 2.  Walk the sorted list merging overlaps
      MergedIterator< Toffs > itr = new MergedIterator<>( false, allToffs );
      termsOffsets = new ArrayList<>();
      if ( !itr.hasNext() ) {
        return;
      }
      Toffs work = itr.next();
      while ( itr.hasNext() ) {
        Toffs current = itr.next();
        if ( current.startOffset <= work.endOffset ) {
          work.endOffset = Math.max( work.endOffset, current.endOffset );
        } else {
          termsOffsets.add( work );
          work = current;
        }
      }
      termsOffsets.add( work );
    }
    
    public int getStartOffset(){
      return termsOffsets.get( 0 ).startOffset;
    }
    
    public int getEndOffset(){
      return termsOffsets.get( termsOffsets.size() - 1 ).endOffset;
    }

    public boolean isOffsetOverlap( WeightedPhraseInfo other ){
      int so = getStartOffset();
      int eo = getEndOffset();
      int oso = other.getStartOffset();
      int oeo = other.getEndOffset();
      if( so <= oso && oso < eo ) return true;
      if( so < oeo && oeo <= eo ) return true;
      if( oso <= so && so < oeo ) return true;
      if( oso < eo && eo <= oeo ) return true;
      return false;
    }
    
    @Override
    public String toString(){
      StringBuilder sb = new StringBuilder();
      sb.append( getText() ).append( '(' ).append( boost ).append( ")(" );
      for( Toffs to : termsOffsets ){
        sb.append( to );
      }
      sb.append( ')' );
      return sb.toString();
    }
    
    /**
     * @return the seqnum
     */
    public int getSeqnum() {
      return seqnum;
    }

    @Override
    public int compareTo( WeightedPhraseInfo other ) {
      int diff = getStartOffset() - other.getStartOffset();
      if ( diff != 0 ) {
        return diff;
      }
      diff = getEndOffset() - other.getEndOffset();
      if ( diff != 0 ) {
        return diff;
      }
      return (int) Math.signum( getBoost() - other.getBoost() );
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + getStartOffset();
      result = prime * result + getEndOffset();
      long b = Double.doubleToLongBits( getBoost() );
      result = prime * result + ( int )( b ^ ( b >>> 32 ) );
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) {
        return true;
      }
      if (obj == null) {
        return false;
      }
      if (getClass() != obj.getClass()) {
        return false;
      }
      WeightedPhraseInfo other = (WeightedPhraseInfo) obj;
      if (getStartOffset() != other.getStartOffset()) {
        return false;
      }
      if (getEndOffset() != other.getEndOffset()) {
        return false;
      }
      if (getBoost() != other.getBoost()) {
        return false;
      }
      return true;
    }

    /**
     * Term offsets (start + end)
     */
    public static class Toffs implements Comparable< Toffs > {
      private int startOffset;
      private int endOffset;
      public Toffs( int startOffset, int endOffset ){
        this.startOffset = startOffset;
        this.endOffset = endOffset;
      }
      public void setEndOffset( int endOffset ){
        this.endOffset = endOffset;
      }
      public int getStartOffset(){
        return startOffset;
      }
      public int getEndOffset(){
        return endOffset;
      }
      @Override
      public int compareTo( Toffs other ) {
        int diff = getStartOffset() - other.getStartOffset();
        if ( diff != 0 ) {
          return diff;
        }
        return getEndOffset() - other.getEndOffset();
      }
      @Override
      public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + getStartOffset();
        result = prime * result + getEndOffset();
        return result;
      }
      @Override
      public boolean equals(Object obj) {
        if (this == obj) {
          return true;
        }
        if (obj == null) {
          return false;
        }
        if (getClass() != obj.getClass()) {
          return false;
        }
        Toffs other = (Toffs) obj;
        if (getStartOffset() != other.getStartOffset()) {
          return false;
        }
        if (getEndOffset() != other.getEndOffset()) {
          return false;
        }
        return true;
      }
      @Override
      public String toString(){
        StringBuilder sb = new StringBuilder();
        sb.append( '(' ).append( startOffset ).append( ',' ).append( endOffset ).append( ')' );
        return sb.toString();
      }
    }
  }
}
