/*
 * 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.queries.function;

import java.io.IOException;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;

import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.DoubleValues;
import org.apache.lucene.search.DoubleValuesSource;
import org.apache.lucene.search.Explanation;
import org.apache.lucene.search.FieldComparator;
import org.apache.lucene.search.FieldComparatorSource;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LongValues;
import org.apache.lucene.search.LongValuesSource;
import org.apache.lucene.search.Scorable;
import org.apache.lucene.search.SimpleFieldComparator;
import org.apache.lucene.search.SortField;

/**
 * Instantiates {@link FunctionValues} for a particular reader.
 * <br>
 * Often used when creating a {@link FunctionQuery}.
 *
 *
 */
public abstract class ValueSource {

  /**
   * Gets the values for this reader and the context that was previously
   * passed to createWeight().  The values must be consumed in a forward
   * docID manner, and you must call this method again to iterate through
   * the values again.
   */
  public abstract FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException;

  @Override
  public abstract boolean equals(Object o);

  @Override
  public abstract int hashCode();

  /**
   * description of field, used in explain()
   */
  public abstract String description();

  @Override
  public String toString() {
    return description();
  }


  /**
   * Implementations should propagate createWeight to sub-ValueSources which can optionally store
   * weight info in the context. The context object will be passed to getValues()
   * where this info can be retrieved.
   */
  public void createWeight(Map context, IndexSearcher searcher) throws IOException {
  }

  /**
   * Returns a new non-threadsafe context map.
   */
  public static Map newContext(IndexSearcher searcher) {
    Map context = new IdentityHashMap();
    context.put("searcher", searcher);
    return context;
  }

  private static class ScoreAndDoc extends Scorable {

    int current = -1;
    float score = 0;

    @Override
    public int docID() {
      return current;
    }

    @Override
    public float score() {
      return score;
    }
  }

  /**
   * Expose this ValueSource as a LongValuesSource
   */
  public LongValuesSource asLongValuesSource() {
    return new WrappedLongValuesSource(this);
  }

  private static class WrappedLongValuesSource extends LongValuesSource {

    private final ValueSource in;

    private WrappedLongValuesSource(ValueSource in) {
      this.in = in;
    }

    @Override
    public LongValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
      Map context = new IdentityHashMap<>();
      ScoreAndDoc scorer = new ScoreAndDoc();
      context.put("scorer", scorer);
      final FunctionValues fv = in.getValues(context, ctx);
      return new LongValues() {

        @Override
        public long longValue() throws IOException {
          return fv.longVal(scorer.current);
        }

        @Override
        public boolean advanceExact(int doc) throws IOException {
          scorer.current = doc;
          if (scores != null && scores.advanceExact(doc))
            scorer.score = (float) scores.doubleValue();
          else
            scorer.score = 0;
          return fv.exists(doc);
        }
      };
    }

    @Override
    public boolean isCacheable(LeafReaderContext ctx) {
      return false;
    }

    @Override
    public boolean needsScores() {
      return false;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      WrappedLongValuesSource that = (WrappedLongValuesSource) o;
      return Objects.equals(in, that.in);
    }

    @Override
    public int hashCode() {
      return Objects.hash(in);
    }

    @Override
    public String toString() {
      return in.toString();
    }

    @Override
    public LongValuesSource rewrite(IndexSearcher searcher) throws IOException {
      return this;
    }

  }

  /**
   * Expose this ValueSource as a DoubleValuesSource
   */
  public DoubleValuesSource asDoubleValuesSource() {
    return new WrappedDoubleValuesSource(this, null);
  }

  static class WrappedDoubleValuesSource extends DoubleValuesSource {

    final ValueSource in;
    final IndexSearcher searcher;

    private WrappedDoubleValuesSource(ValueSource in, IndexSearcher searcher) {
      this.in = in;
      this.searcher = searcher;
    }

    @Override
    public DoubleValues getValues(LeafReaderContext ctx, DoubleValues scores) throws IOException {
      Map context = new HashMap<>();
      ScoreAndDoc scorer = new ScoreAndDoc();
      context.put("scorer", scorer);
      context.put("searcher", searcher);
      FunctionValues fv = in.getValues(context, ctx);
      return new DoubleValues() {

        @Override
        public double doubleValue() throws IOException {
          return fv.doubleVal(scorer.current);
        }

        @Override
        public boolean advanceExact(int doc) throws IOException {
          scorer.current = doc;
          if (scores != null && scores.advanceExact(doc)) {
            scorer.score = (float) scores.doubleValue();
          }
          else
            scorer.score = 0;
          // ValueSource will return values even if exists() is false, generally a default
          // of some kind.  To preserve this behaviour with the iterator, we need to always
          // return 'true' here.
          return true;
        }
      };
    }

    @Override
    public boolean needsScores() {
      return true;  // be on the safe side
    }

    @Override
    public boolean isCacheable(LeafReaderContext ctx) {
      return false;
    }

    @Override
    public Explanation explain(LeafReaderContext ctx, int docId, Explanation scoreExplanation) throws IOException {
      Map context = new HashMap<>();
      ScoreAndDoc scorer = new ScoreAndDoc();
      scorer.score = scoreExplanation.getValue().floatValue();
      context.put("scorer", scorer);
      context.put("searcher", searcher);
      FunctionValues fv = in.getValues(context, ctx);
      return fv.explain(docId);
    }

    @Override
    public DoubleValuesSource rewrite(IndexSearcher searcher) throws IOException {
      return new WrappedDoubleValuesSource(in, searcher);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      WrappedDoubleValuesSource that = (WrappedDoubleValuesSource) o;
      return Objects.equals(in, that.in);
    }

    @Override
    public int hashCode() {
      return Objects.hash(in);
    }

    @Override
    public String toString() {
      return in.toString();
    }

  }

  public static ValueSource fromDoubleValuesSource(DoubleValuesSource in) {
    return new FromDoubleValuesSource(in);
  }

  private static class FromDoubleValuesSource extends ValueSource {

    final DoubleValuesSource in;

    private FromDoubleValuesSource(DoubleValuesSource in) {
      this.in = in;
    }

    @Override
    public FunctionValues getValues(Map context, LeafReaderContext readerContext) throws IOException {
      Scorable scorer = (Scorable) context.get("scorer");
      DoubleValues scores = scorer == null ? null : DoubleValuesSource.fromScorer(scorer);

      IndexSearcher searcher = (IndexSearcher) context.get("searcher");
      DoubleValues inner;
      if (searcher != null)
        inner = in.rewrite(searcher).getValues(readerContext, scores);
      else
        inner = in.getValues(readerContext, scores);

      return new FunctionValues() {
        @Override
        public String toString(int doc) throws IOException {
          return in.toString();
        }

        @Override
        public float floatVal(int doc) throws IOException {
          if (inner.advanceExact(doc) == false)
            return 0;
          return (float) inner.doubleValue();
        }

        @Override
        public double doubleVal(int doc) throws IOException {
          if (inner.advanceExact(doc) == false)
            return 0;
          return inner.doubleValue();
        }

        @Override
        public boolean exists(int doc) throws IOException {
          return inner.advanceExact(doc);
        }
      };
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      FromDoubleValuesSource that = (FromDoubleValuesSource) o;
      return Objects.equals(in, that.in);
    }

    @Override
    public int hashCode() {
      return Objects.hash(in);
    }

    @Override
    public String description() {
      return in.toString();
    }

  }

  //
  // Sorting by function
  //

  /**
   * EXPERIMENTAL: This method is subject to change.
   * <p>
   * Get the SortField for this ValueSource.  Uses the {@link #getValues(java.util.Map, org.apache.lucene.index.LeafReaderContext)}
   * to populate the SortField.
   *
   * @param reverse true if this is a reverse sort.
   * @return The {@link org.apache.lucene.search.SortField} for the ValueSource
   */
  public SortField getSortField(boolean reverse) {
    return new ValueSourceSortField(reverse);
  }

  class ValueSourceSortField extends SortField {
    public ValueSourceSortField(boolean reverse) {
      super(description(), SortField.Type.REWRITEABLE, reverse);
    }

    @Override
    public SortField rewrite(IndexSearcher searcher) throws IOException {
      Map context = newContext(searcher);
      createWeight(context, searcher);
      return new SortField(getField(), new ValueSourceComparatorSource(context), getReverse());
    }
  }

  class ValueSourceComparatorSource extends FieldComparatorSource {
    private final Map context;

    public ValueSourceComparatorSource(Map context) {
      this.context = context;
    }

    @Override
    public FieldComparator<Double> newComparator(String fieldname, int numHits,
                                         int sortPos, boolean reversed) {
      return new ValueSourceComparator(context, numHits);
    }
  }

  /**
   * Implement a {@link org.apache.lucene.search.FieldComparator} that works
   * off of the {@link FunctionValues} for a ValueSource
   * instead of the normal Lucene FieldComparator that works off of a FieldCache.
   */
  class ValueSourceComparator extends SimpleFieldComparator<Double> {
    private final double[] values;
    private FunctionValues docVals;
    private double bottom;
    private final Map fcontext;
    private double topValue;

    ValueSourceComparator(Map fcontext, int numHits) {
      this.fcontext = fcontext;
      values = new double[numHits];
    }

    @Override
    public int compare(int slot1, int slot2) {
      return Double.compare(values[slot1], values[slot2]);
    }

    @Override
    public int compareBottom(int doc) throws IOException {
      return Double.compare(bottom, docVals.doubleVal(doc));
    }

    @Override
    public void copy(int slot, int doc) throws IOException {
      values[slot] = docVals.doubleVal(doc);
    }

    @Override
    public void doSetNextReader(LeafReaderContext context) throws IOException {
      docVals = getValues(fcontext, context);
    }

    @Override
    public void setBottom(final int bottom) {
      this.bottom = values[bottom];
    }

    @Override
    public void setTopValue(final Double value) {
      this.topValue = value.doubleValue();
    }

    @Override
    public Double value(int slot) {
      return values[slot];
    }

    @Override
    public int compareTop(int doc) throws IOException {
      final double docValue = docVals.doubleVal(doc);
      return Double.compare(topValue, docValue);
    }
  }
}
