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

import java.util.ArrayList;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

import org.apache.solr.analytics.facet.RangeFacet;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.FacetParams.FacetRangeInclude;
import org.apache.solr.common.params.FacetParams.FacetRangeOther;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.NumericFieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.util.DateMathParser;


/**
 * Calculates a set of {@link FacetRange}s for a given {@link RangeFacet}.
 */
public abstract class FacetRangeGenerator<T extends Comparable<T>> {
  protected final SchemaField field;
  protected final RangeFacet rangeFacet;
  
  public FacetRangeGenerator(final RangeFacet rangeFacet) {
    this.field = rangeFacet.getField();
    this.rangeFacet = rangeFacet;
  }

  /**
   * Formats a Range endpoint for use as a range label name in the response.
   * Default Impl just uses toString()
   */
  public String formatValue(final T val) {
    return val.toString();
  }
  
  /**
   * Parses a String param into an Range endpoint value throwing 
   * a useful exception if not possible
   */
  public final T getValue(final String rawval) {
    try {
      return parseVal(rawval);
    } catch (Exception e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't parse value "+rawval+" for field: " + field.getName(), e);
    }
  }
  
  /**
   * Parses a String param into an Range endpoint. 
   * Can throw a low level format exception as needed.
   */
  protected abstract T parseVal(final String rawval) throws java.text.ParseException;

  /** 
   * Parses a String param into a value that represents the gap and 
   * can be included in the response, throwing 
   * a useful exception if not possible.
   *
   * Note: uses Object as the return type instead of T for things like 
   * Date where gap is just a DateMathParser string 
   */
  public final Object getGap(final String gap) {
    try {
      return parseGap(gap);
    } catch (Exception e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't parse gap "+gap+" for field: " + field.getName(), e);
    }
  }

  /**
   * Parses a String param into a value that represents the gap and 
   * can be included in the response. 
   * Can throw a low level format exception as needed.
   *
   * Default Impl calls parseVal
   */
  protected Object parseGap(final String rawval) throws java.text.ParseException {
    return parseVal(rawval);
  }

  /**
   * Adds the String gap param to a low Range endpoint value to determine 
   * the corrisponding high Range endpoint value, throwing 
   * a useful exception if not possible.
   */
  public final T addGap(T value, String gap) {
    try {
      return parseAndAddGap(value, gap);
    } catch (Exception e) {
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Can't add gap "+gap+" to value " + value + " for field: " + field.getName(), e);
    }
  }
  
  /**
   * Adds the String gap param to a low Range endpoint value to determine 
   * the corrisponding high Range endpoint value.
   * Can throw a low level format exception as needed.
   */
  protected abstract T parseAndAddGap(T value, String gap) throws java.text.ParseException;

  public static class FacetRange {
    public final String name;
    public final String lower;
    public final String upper;
    public final boolean includeLower;
    public final boolean includeUpper;
    private final String facetValue;
    
    public FacetRange(String name, String lower, String upper, boolean includeLower, boolean includeUpper) {
      this.name = name;
      this.lower = lower;
      this.upper = upper;
      this.includeLower = includeLower;
      this.includeUpper = includeUpper;
      
      String value = "(*";
      if (lower != null) {
        value = (includeLower ? "[" : "(") + lower;
      }
      value += " TO ";
      if (upper == null) {
        value += "*)";
      } else {
        value += upper + (includeUpper? "]" : ")");
      }
      facetValue = value;
    }
    
    @Override
    public String toString() {
        return facetValue;
    }
  }
  
  public List<FacetRange> getRanges(){

    final T start = getValue(rangeFacet.getStart());
    T end = getValue(rangeFacet.getEnd()); // not final, hardend may change this
    
    if( end.compareTo(start) < 0 ){
      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "range facet 'end' comes before 'start': "+end+" < "+start);
    }
    
    // explicitly return the gap.  compute this early so we are more 
    // likely to catch parse errors before attempting math
    final List<String> gaps = rangeFacet.getGaps();
    String gap = gaps.get(0);
    
    final EnumSet<FacetRangeInclude> include = rangeFacet.getInclude();
        
    T low = start;
    
    List<FacetRange> ranges = new ArrayList<>();
    
    int gapCounter = 0;
    
    while (low.compareTo(end) < 0) {
      if (gapCounter<gaps.size()) {
        gap = gaps.get(gapCounter++);
      }
      T high = addGap(low,gap);
      if (end.compareTo(high) < 0) {
        if (rangeFacet.isHardEnd()){
          high = end;
        } else {
          end = high;
        }
      }
      
      if (high.compareTo(low) < 0) {
        throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop (is gap negative? did the math overflow?)");
      }
      
      if (high.compareTo(low) == 0) {
        throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "range facet infinite loop: gap is either zero, or too small relative start/end and caused underflow: " + low + " + " + gap + " = " + high );
      }
      
      final boolean includeLower = (include.contains(FacetRangeInclude.ALL) ||
                                    include.contains(FacetRangeInclude.LOWER) ||
                                   (include.contains(FacetRangeInclude.EDGE) && 
                                   0 == low.compareTo(start)));
      final boolean includeUpper = (include.contains(FacetRangeInclude.ALL) ||
                                    include.contains(FacetRangeInclude.UPPER) ||
                                   (include.contains(FacetRangeInclude.EDGE) && 
                                   0 == high.compareTo(end)));
      
      final String lowS = formatValue(low);
      final String highS = formatValue(high);

      ranges.add( new FacetRange(lowS,lowS,highS,includeLower,includeUpper) );
      low = high;
    }
    
    final Set<FacetRangeOther> others = rangeFacet.getOthers();
    if (null != others && 0 < others.size() ) {
      
      // no matter what other values are listed, we don't do
      // anything if "none" is specified.
      if( !others.contains(FacetRangeOther.NONE) ) {
        
        boolean all = others.contains(FacetRangeOther.ALL);

        if (all || others.contains(FacetRangeOther.BEFORE)) {
          // include upper bound if "outer" or if first gap doesn't already include it
          ranges.add( new FacetRange(FacetRangeOther.BEFORE.toString(), 
                                        null, formatValue(start), false, include.contains(FacetRangeInclude.OUTER) || include.contains(FacetRangeInclude.ALL) ||
                                                            !(include.contains(FacetRangeInclude.LOWER) || include.contains(FacetRangeInclude.EDGE)) ) );
          
        }
        if (all || others.contains(FacetRangeOther.AFTER)) {
          // include lower bound if "outer" or if last gap doesn't already include it
          ranges.add( new FacetRange(FacetRangeOther.AFTER.toString(), 
                                        formatValue(end), null, include.contains(FacetRangeInclude.OUTER) || include.contains(FacetRangeInclude.ALL) ||
                                                   !(include.contains(FacetRangeInclude.UPPER) || include.contains(FacetRangeInclude.EDGE)), false) );
        }
        if (all || others.contains(FacetRangeOther.BETWEEN)) {
          ranges.add( new FacetRange(FacetRangeOther.BETWEEN.toString(), formatValue(start), formatValue(end),
                                        include.contains(FacetRangeInclude.LOWER) || include.contains(FacetRangeInclude.EDGE) || include.contains(FacetRangeInclude.ALL),
                                        include.contains(FacetRangeInclude.UPPER) || include.contains(FacetRangeInclude.EDGE) || include.contains(FacetRangeInclude.ALL)) );
        }
      }
      
    }
  
    return ranges;
  }
  
  public static FacetRangeGenerator<? extends Comparable<?>> create(RangeFacet rangeFacet){
    final SchemaField sf = rangeFacet.getField();
    final FieldType ft = sf.getType();
    final FacetRangeGenerator<?> calc;
    if (ft instanceof NumericFieldType) {
      switch (ft.getNumberType()) {
        case FLOAT:
          calc = new FloatFacetRangeGenerator(rangeFacet);
          break;
        case DOUBLE:
          calc = new DoubleFacetRangeGenerator(rangeFacet);
          break;
        case INTEGER:
          calc = new IntegerFacetRangeGenerator(rangeFacet);
          break;
        case LONG:
          calc = new LongFacetRangeGenerator(rangeFacet);
          break;
        case DATE:
          calc = new DateFacetRangeGenerator(rangeFacet, null);
          break;
        default:
          throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on numeric field of unexpected type: " + sf.getName());
      }
    } else {
      throw new SolrException (SolrException.ErrorCode.BAD_REQUEST, "Unable to range facet on non-numeric field: " + sf);
    } 
    return calc;
  }
}
class IntegerFacetRangeGenerator extends FacetRangeGenerator<Integer> {
  public IntegerFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }

  @Override
  protected Integer parseVal(String rawval) {
    return Integer.valueOf(rawval);
  }
  @Override
  public Integer parseAndAddGap(Integer value, String gap) {
    return value.intValue() + Integer.valueOf(gap).intValue();
  }
}
class LongFacetRangeGenerator extends FacetRangeGenerator<Long> {
  public LongFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }

  @Override
  protected Long parseVal(String rawval) {
    return Long.valueOf(rawval);
  }
  @Override
  public Long parseAndAddGap(Long value, String gap) {
    return value.longValue() + Long.valueOf(gap).longValue();
  }
}

class FloatFacetRangeGenerator extends FacetRangeGenerator<Float> {
  public FloatFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }

  @Override
  protected Float parseVal(String rawval) {
    return Float.valueOf(rawval);
  }
  @Override
  public Float parseAndAddGap(Float value, String gap) {
    return value.floatValue() + Float.valueOf(gap).floatValue();
  }
}

class DoubleFacetRangeGenerator extends FacetRangeGenerator<Double> {
  public DoubleFacetRangeGenerator(final RangeFacet rangeFacet) { super(rangeFacet); }

  @Override
  protected Double parseVal(String rawval) {
    return Double.valueOf(rawval);
  }
  @Override
  public Double parseAndAddGap(Double value, String gap) {
    return value.doubleValue() + Double.valueOf(gap).doubleValue();
  }
}
class DateFacetRangeGenerator extends FacetRangeGenerator<Date> {
  private final Date now;
  public DateFacetRangeGenerator(final RangeFacet rangeFacet, final Date now) { 
    super(rangeFacet); 
    this.now = now;
  }
  
  @Override
  public String formatValue(Date val) {
    return val.toInstant().toString();
  }
  @Override
  protected Date parseVal(String rawval) {
    return DateMathParser.parseMath(now, rawval);
  }
  @Override
  protected Object parseGap(final String rawval) {
    return rawval;
  }
  @Override
  public Date parseAndAddGap(Date value, String gap) throws java.text.ParseException {
    final DateMathParser dmp = new DateMathParser();
    dmp.setNow(value);
    return dmp.parseMath(gap);
  }
}
