/*
 * 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.geode.cache.lucene.internal;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import org.apache.geode.cache.Region;
import org.apache.geode.cache.execute.Execution;
import org.apache.geode.cache.execute.FunctionException;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.ResultCollector;
import org.apache.geode.cache.lucene.LuceneResultStruct;
import org.apache.geode.cache.lucene.PageableLuceneQueryResults;
import org.apache.geode.cache.lucene.internal.distributed.EntryScore;
import org.apache.geode.cache.lucene.internal.results.LuceneGetPageFunction;
import org.apache.geode.cache.lucene.internal.results.MapResultCollector;

/**
 * Implementation of PageableLuceneQueryResults that fetches a page at a time from the server, given
 * a set of EntryScores (key and score).
 *
 * @param <K> The type of the key
 * @param <V> The type of the value
 */
public class PageableLuceneQueryResultsImpl<K, V> implements PageableLuceneQueryResults<K, V> {

  /**
   * list of docs matching search query
   */
  private final List<EntryScore<K>> hits;

  /**
   * * Current page of results
   */
  private List<LuceneResultStruct<K, V>> currentPage;
  /**
   * The maximum score. Lazily evaluated
   */
  private float maxScore = Float.MIN_VALUE;

  /**
   * The user region where values are stored.
   */
  private final Region<K, V> userRegion;

  /**
   * The start of the next page of results we want to fetch
   */
  private int currentHit = 0;

  /**
   * The page size the user wants.
   */
  private final int pageSize;

  public PageableLuceneQueryResultsImpl(List<EntryScore<K>> hits, Region<K, V> userRegion,
      int pageSize) {
    this.hits = hits;
    this.userRegion = userRegion;
    this.pageSize = pageSize == 0 ? Integer.MAX_VALUE : pageSize;
  }


  public List<LuceneResultStruct<K, V>> getHitEntries(int fromIndex, int toIndex) {
    ArrayList<LuceneResultStruct<K, V>> results = null;
    try {
      List<EntryScore<K>> scores = hits.subList(fromIndex, toIndex);
      Set<K> keys = new HashSet<>(scores.size());
      for (EntryScore<K> score : scores) {
        keys.add(score.getKey());
      }

      Map<K, V> values = getValues(keys);


      results = new ArrayList<>(values.size());
      for (EntryScore<K> score : scores) {
        V value = values.get(score.getKey());
        if (value != null) {
          results.add(new LuceneResultStructImpl(score.getKey(), value, score.getScore()));
        }
      }
    } catch (FunctionException functionException) {
      if (functionException.getCause() instanceof RuntimeException) {
        throw (RuntimeException) functionException.getCause();
      }
      throw functionException;
    }
    return results;
  }

  protected Map<K, V> getValues(final Set<K> keys) {
    ResultCollector resultCollector = onRegion().withFilter(keys)
        .withCollector(new MapResultCollector()).execute(LuceneGetPageFunction.ID);
    return (Map<K, V>) resultCollector.getResult();
  }

  protected Execution onRegion() {
    return FunctionService.onRegion(userRegion);
  }

  @Override
  public List<LuceneResultStruct<K, V>> next() {
    if (!hasNext()) {
      throw new NoSuchElementException();
    }
    List<LuceneResultStruct<K, V>> result = advancePage();
    currentPage = null;
    return result;
  }

  private List<LuceneResultStruct<K, V>> advancePage() {
    if (currentPage != null) {
      return currentPage;
    }

    int resultSize = (pageSize != Integer.MAX_VALUE) ? pageSize : hits.size();
    currentPage = new ArrayList<>(resultSize);
    while (currentPage.size() < pageSize && currentHit < hits.size()) {
      int end = currentHit + pageSize - currentPage.size();
      end = end > hits.size() ? hits.size() : end;
      currentPage.addAll(getHitEntries(currentHit, end));
      currentHit = end;
    }
    return currentPage;
  }

  @Override
  public boolean hasNext() {
    advancePage();
    return !currentPage.isEmpty();
  }

  @Override
  public int size() {
    return hits.size();
  }

  @Override
  public float getMaxScore() {
    if (maxScore == Float.MIN_VALUE) {
      for (EntryScore<K> score : hits) {
        maxScore = Math.max(maxScore, score.getScore());
      }
    }

    return maxScore;
  }
}
