/*
 * 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.solr.analytics.function.mapping;

import org.apache.solr.analytics.ExpressionFactory.CreatorFunction;
import org.apache.solr.analytics.function.mapping.ComparisonFunction.CompResultFunction;
import org.apache.solr.analytics.util.function.BooleanConsumer;
import org.apache.solr.analytics.value.AnalyticsValue;
import org.apache.solr.analytics.value.AnalyticsValueStream;
import org.apache.solr.analytics.value.BooleanValue;
import org.apache.solr.analytics.value.BooleanValueStream;
import org.apache.solr.analytics.value.DateValue;
import org.apache.solr.analytics.value.DateValueStream;
import org.apache.solr.analytics.value.DoubleValue;
import org.apache.solr.analytics.value.DoubleValueStream;
import org.apache.solr.analytics.value.BooleanValue.AbstractBooleanValue;
import org.apache.solr.analytics.value.BooleanValueStream.AbstractBooleanValueStream;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;

/**
 * Contains all comparable functions. Comparable functions accept two comparable (numeric and date) parameters and return a BooleanValueStream.
 * The two parameters must be able to be cast to the same type.
 * <p>
 * Uses:
 * <ul>
 * <li>If a two comparable {@link AnalyticsValue}s are passed in, a {@link BooleanValue} representing the comparison of the two values for each document is returned.
 * <li>If a comparable {@link AnalyticsValue} and a comparable {@link AnalyticsValueStream} are passed in, 
 * a {@link BooleanValueStream} representing the comparison of the Value and each of the values of the ValueStream for the document is returned.
 * </ul>
 */
public class ComparisonFunction {
  
  /**
   * Create a comparison mapping function, comparing two analytics value (streams) of the same type.
   * 
   * @param name name of the function
   * @param comp function to find the result of a comparison
   * @param params the parameters to compare
   * @return an instance of the requested comparison function using the given parameters
   */
  public static BooleanValueStream createComparisonFunction(String name, CompResultFunction comp, AnalyticsValueStream... params) {
    if (params.length != 2) {
      throw new SolrException(ErrorCode.BAD_REQUEST,"The "+name+" function requires 2 paramaters, " + params.length + " found.");
    }
    AnalyticsValueStream paramA = params[0];
    AnalyticsValueStream paramB = params[1];
    if (paramA instanceof DoubleValueStream && paramB instanceof DoubleValueStream) {
      if (paramA instanceof DoubleValue) {
        if (paramB instanceof DoubleValue) {
          return new CompareDoubleValueFunction(name,(DoubleValue)paramA,(DoubleValue)paramB,comp);
        }
        return new CompareDoubleStreamFunction(name,(DoubleValue)paramA,(DoubleValueStream)paramB,comp);
      }
      if (paramB instanceof DoubleValue) {
        return new CompareDoubleStreamFunction(name,(DoubleValue)paramB,(DoubleValueStream)paramA,reverse(comp));
      }
    } else if (paramA instanceof DateValueStream && paramB instanceof DateValueStream) {
      if (paramA instanceof DateValue) {
        if (paramB instanceof DateValue) {
          return new CompareDateValueFunction(name,(DateValue)paramA,(DateValue)paramB,comp);
        }
        return new CompareDateStreamFunction(name,(DateValue)paramA,(DateValueStream)paramB,comp);
      }
      if (paramB instanceof DateValue) {
        return new CompareDateStreamFunction(name,(DateValue)paramB,(DateValueStream)paramA,reverse(comp));
      }
    } else {
      throw new SolrException(ErrorCode.BAD_REQUEST,"The "+name+" function requires comparable (numeric or date) parameters.");
    }
    throw new SolrException(ErrorCode.BAD_REQUEST,"The "+name+" function requires that at least 1 parameter be single-valued.");
  }
  
  /**
   * A comparison function that tests whether the first parameter is greater than the second parameter
   */
  public static class GTFunction {
    public static final String name = "gt";
    public static final CreatorFunction creatorFunction = (params -> {
      return ComparisonFunction.createComparisonFunction(name, val -> {
        return val > 0;
      }, params);
    });
  }

  /**
   * A comparison function that tests whether the first parameter is greater than or equal to the second parameter
   */
  public static class GTEFunction {
    public static final String name = "gte";
    public static final CreatorFunction creatorFunction = (params -> {
      return ComparisonFunction.createComparisonFunction(name, val -> {
        return val >= 0;
      }, params);
    });
  }

  /**
   * A comparison function that tests whether the first parameter is less than the second parameter
   */
  public static class LTFunction {
    public static final String name = "lt";
    public static final CreatorFunction creatorFunction = (params -> {
      return ComparisonFunction.createComparisonFunction(name, val -> {
        return val < 0;
      }, params);
    });
  }

  /**
   * A comparison function that tests whether the first parameter is less than or equal to the second parameter
   */
  public static class LTEFunction {
    public static final String name = "lte";
    public static final CreatorFunction creatorFunction = (params -> {
      return ComparisonFunction.createComparisonFunction(name, val -> {
        return val <= 0;
      }, params);
    });
  }

  @FunctionalInterface
  public static interface CompResultFunction {
    public boolean apply(int compResult);
  }
  
  private static CompResultFunction reverse(CompResultFunction original) {
    return val -> original.apply(val*-1);
  }
}
/**
 * A comparison function for two {@link DoubleValue}s.
 */
class CompareDoubleValueFunction extends AbstractBooleanValue {
  private final DoubleValue exprA;
  private final DoubleValue exprB;
  private final CompResultFunction comp;
  private final String name;
  private final String funcStr;
  private final ExpressionType funcType;
  
  public CompareDoubleValueFunction(String name, DoubleValue exprA, DoubleValue exprB, CompResultFunction comp) {
    this.name = name;
    this.exprA = exprA;
    this.exprB = exprB;
    this.comp = comp;
    this.funcStr = AnalyticsValueStream.createExpressionString(name,exprA,exprB);
    this.funcType = AnalyticsValueStream.determineMappingPhase(funcStr,exprA,exprB);
  }

  private boolean exists = false;
  @Override
  public boolean getBoolean() {
    double valueA = exprA.getDouble();
    double valueB = exprB.getDouble();
    exists = exprA.exists() && exprB.exists();
    return exists ? comp.apply(Double.compare(valueA,valueB)) : false;
  }
  @Override
  public boolean exists() {
    return exists;
  }

  @Override
  public String getName() {
    return name;
  }
  @Override
  public String getExpressionStr() {
    return funcStr;
  }
  @Override
  public ExpressionType getExpressionType() {
    return funcType;
  }
}
/**
 * A comparison function for a {@link DoubleValue} and a {@link DoubleValueStream}.
 */
class CompareDoubleStreamFunction extends AbstractBooleanValueStream {
  private final DoubleValue baseExpr;
  private final DoubleValueStream compExpr;
  private final CompResultFunction comp;
  private final String name;
  private final String funcStr;
  private final ExpressionType funcType;
  
  public CompareDoubleStreamFunction(String name, DoubleValue baseExpr, DoubleValueStream compExpr, CompResultFunction comp) throws SolrException {
    this.name = name;
    this.baseExpr = baseExpr;
    this.compExpr = compExpr;
    this.comp = comp;
    this.funcStr = AnalyticsValueStream.createExpressionString(name,baseExpr,compExpr);
    this.funcType = AnalyticsValueStream.determineMappingPhase(funcStr,baseExpr,compExpr);
  }

  @Override
  public void streamBooleans(BooleanConsumer cons) {
    double baseValue = baseExpr.getDouble();
    if (baseExpr.exists()) {
      compExpr.streamDoubles(compValue -> cons.accept(comp.apply(Double.compare(baseValue,compValue))));
    }
  }

  @Override
  public String getName() {
    return name;
  }
  @Override
  public String getExpressionStr() {
    return funcStr;
  }
  @Override
  public ExpressionType getExpressionType() {
    return funcType;
  }
}
/**
 * A comparison function for two {@link DateValue}s.
 */
class CompareDateValueFunction extends AbstractBooleanValue {
  private final DateValue exprA;
  private final DateValue exprB;
  private final CompResultFunction comp;
  private final String name;
  private final String funcStr;
  private final ExpressionType funcType;
  
  public CompareDateValueFunction(String name, DateValue exprA, DateValue exprB, CompResultFunction comp) {
    this.name = name;
    this.exprA = exprA;
    this.exprB = exprB;
    this.comp = comp;
    this.funcStr = AnalyticsValueStream.createExpressionString(name,exprA,exprB);
    this.funcType = AnalyticsValueStream.determineMappingPhase(funcStr,exprA,exprB);
  }

  private boolean exists = false;
  @Override
  public boolean getBoolean() {
    long valueA = exprA.getLong();
    long valueB = exprB.getLong();
    exists = exprA.exists() && exprB.exists();
    return exists ? comp.apply(Long.compare(valueA,valueB)) : false;
  }
  @Override
  public boolean exists() {
    return exists;
  }

  @Override
  public String getName() {
    return name;
  }
  @Override
  public String getExpressionStr() {
    return funcStr;
  }
  @Override
  public ExpressionType getExpressionType() {
    return funcType;
  }
}
/**
 * A comparison function for a {@link DateValue} and a {@link DateValueStream}.
 */
class CompareDateStreamFunction extends AbstractBooleanValueStream {
  private final DateValue baseExpr;
  private final DateValueStream compExpr;
  private final CompResultFunction comp;
  private final String name;
  private final String funcStr;
  private final ExpressionType funcType;
  
  public CompareDateStreamFunction(String name, DateValue baseExpr, DateValueStream compExpr, CompResultFunction comp) throws SolrException {
    this.name = name;
    this.baseExpr = baseExpr;
    this.compExpr = compExpr;
    this.comp = comp;
    this.funcStr = AnalyticsValueStream.createExpressionString(name,baseExpr,compExpr);
    this.funcType = AnalyticsValueStream.determineMappingPhase(funcStr,baseExpr,compExpr);
  }

  @Override
  public void streamBooleans(BooleanConsumer cons) {
    long baseValue = baseExpr.getLong();
    if (baseExpr.exists()) {
      compExpr.streamLongs(compValue -> cons.accept(comp.apply(Long.compare(baseValue,compValue))));
    }
  }

  @Override
  public String getName() {
    return name;
  }
  @Override
  public String getExpressionStr() {
    return funcStr;
  }
  @Override
  public ExpressionType getExpressionType() {
    return funcType;
  }
}