/*
 * 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.client.solrj.cloud.autoscaling;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.apache.solr.common.MapWriter;
import org.apache.solr.common.util.StrUtils;
import org.apache.solr.common.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Preference implements MapWriter {
  private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

  final Policy.SortParam name;
  Integer precision;
  final Policy.Sort sort;
  Preference next;
  final int idx;
  @SuppressWarnings({"rawtypes"})
  private final Map original;

  public Preference(Map<String, Object> m) {
    this(m, 0);
  }

  public Preference(Map<String, Object> m, int idx) {
    this.idx = idx;
    this.original = Utils.getDeepCopy(m, 3);
    sort = Policy.Sort.get(m);
    name = Policy.SortParam.get(m.get(sort.name()).toString());
    Object p = m.getOrDefault("precision", 0);
    precision = p instanceof Number ? ((Number) p).intValue() : Integer.parseInt(p.toString());
    if (precision < 0) {
      throw new RuntimeException("precision must be a positive value ");
    }
    if (precision < name.min || precision > name.max) {
      throw new RuntimeException(StrUtils.formatString("invalid precision value {0} , must lie between {1} and {2}",
          precision, name.min, name.max));
    }

  }

  // there are 2 modes of compare.
  // recursive, it uses the precision to tie & when there is a tie use the next preference to compare
  // in non-recursive mode, precision is not taken into consideration and sort is done on actual value
  int compare(Row r1, Row r2, boolean useApprox) {
    Object o1 = useApprox ? r1.cells[idx].approxVal : r1.cells[idx].val;
    Object o2 = useApprox ? r2.cells[idx].approxVal : r2.cells[idx].val;
    int result = 0;
    if (o1 instanceof Long && o2 instanceof Long) result = ((Long) o1).compareTo((Long) o2);
    else if (o1 instanceof Double && o2 instanceof Double) {
      result = compareWithTolerance((Double) o1, (Double) o2, useApprox ? 1f : 0.01f);
    } else if (!o1.getClass().getName().equals(o2.getClass().getName())) {
      throw new RuntimeException("Unable to compare " + o1 + " of type: " + o1.getClass().getName() + " from " + r1.cells[idx].toString() + " and " + o2 + " of type: " + o2.getClass().getName() + " from " + r2.cells[idx].toString());
    }
    return result == 0 ?
        (next == null ? 0 :
            next.compare(r1, r2, useApprox)) : sort.sortval * result;
  }

  static int compareWithTolerance(Double o1, Double o2, float percentage) {
    if (percentage == 0) return o1.compareTo(o2);
    if (o1.equals(o2)) return 0;
    double delta = Math.abs(o1 - o2);
    if ((100 * delta / o1) < percentage) return 0;
    return o1.compareTo(o2);
  }

  //sets the new value according to precision in val_
  void setApproxVal(List<Row> tmpMatrix) {
    Object prevVal = null;
    for (Row row : tmpMatrix) {
      if (!row.isLive) {
        continue;
      }
      if (prevVal == null) {//this is the first
        prevVal = row.cells[idx].approxVal = row.cells[idx].val;
      } else {
        double prevD = ((Number) prevVal).doubleValue();
        double currD = ((Number) row.cells[idx].val).doubleValue();
        if (Math.abs(prevD - currD) >= precision) {
          prevVal = row.cells[idx].approxVal = row.cells[idx].val;
        } else {
          prevVal = row.cells[idx].approxVal = prevVal;
        }
      }
    }
  }

  @Override
  public void writeMap(EntryWriter ew) throws IOException {
    for (Object o : original.entrySet()) {
      @SuppressWarnings({"rawtypes"})
      Map.Entry e = (Map.Entry) o;
      ew.put(String.valueOf(e.getKey()), e.getValue());
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Preference that = (Preference) o;

    if (idx != that.idx) return false;
    if (getName() != that.getName()) return false;
    if (precision != null ? !precision.equals(that.precision) : that.precision != null) return false;
    if (sort != that.sort) return false;
    if (next != null ? !next.equals(that.next) : that.next != null) return false;
    return original.equals(that.original);
  }

  @Override
  public int hashCode() {
    return Objects.hash(getName(), precision, sort, idx);
  }

  public Policy.SortParam getName() {
    return name;
  }

  @Override
  public String toString() {
    return Utils.toJSONString(this);
  }

  /**
   * @return an unmodifiable copy of the original map from which this object was constructed
   */
  @SuppressWarnings({"unchecked", "rawtypes"})
  public Map getOriginal() {
    return Collections.unmodifiableMap(original);
  }
}