/*
 * 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.hadoop.hdds.scm.container.placement.metrics;

import com.google.common.annotations.VisibleForTesting;
import java.util.Objects;

/**
 * SCM Node Metric that is used in the placement classes.
 */
public class SCMNodeMetric implements DatanodeMetric<SCMNodeStat, Long>,
    Comparable<SCMNodeMetric> {
  private SCMNodeStat stat;

  /**
   * Constructs an SCMNode Metric.
   *
   * @param stat - SCMNodeStat.
   */
  public SCMNodeMetric(SCMNodeStat stat) {
    this.stat = Objects.requireNonNull(stat, "stat == null");
  }

  /**
   * Set the capacity, used, remaining and committed space on a datanode.
   *
   * @param capacity  in bytes
   * @param used      in bytes
   * @param remaining in bytes
   * @param committed in bytes
   */
  @VisibleForTesting
  public SCMNodeMetric(long capacity, long used, long remaining,
                       long committed, long freeSpaceToSpare, long reserved) {
    this.stat = new SCMNodeStat();
    this.stat.set(capacity, used, remaining, committed, freeSpaceToSpare, reserved);
  }

  /**
   *
   * @param o - Other Object
   * @return - True if *this* object used space weight is greater than argument.
   */
  @Override
  public boolean isGreater(SCMNodeStat o) {
    Objects.requireNonNull(o, "o == null");

    // if zero, replace with 1 for the division to work.
    long thisDenominator = (this.stat.getCapacity().get() == 0)
        ? 1 : this.stat.getCapacity().get();
    long otherDenominator = (o.getCapacity().get() == 0)
        ? 1 : o.getCapacity().get();

    float thisNodeWeight =
        stat.getScmUsed().get() / (float) thisDenominator;

    float oNodeWeight =
        o.getScmUsed().get() / (float) otherDenominator;

    if (Math.abs(thisNodeWeight - oNodeWeight) > 0.000001) {
      return thisNodeWeight > oNodeWeight;
    }
    // if these nodes have similar weight then return the node with more
    // used space as the greater node.
    return stat.getScmUsed().isGreater(o.getScmUsed().get());
  }

  /**
   * Inverse of isGreater.
   *
   * @param o - other object.
   * @return True if *this* object used space weight is Lesser than argument.
   */
  @Override
  public boolean isLess(SCMNodeStat o) {
    Objects.requireNonNull(o, "Argument cannot be null");

    // if zero, replace with 1 for the division to work.
    long thisDenominator = (this.stat.getCapacity().get() == 0)
        ? 1 : this.stat.getCapacity().get();
    long otherDenominator = (o.getCapacity().get() == 0)
        ? 1 : o.getCapacity().get();

    float thisNodeWeight =
        stat.getScmUsed().get() / (float) thisDenominator;

    float oNodeWeight =
        o.getScmUsed().get() / (float) otherDenominator;

    if (Math.abs(thisNodeWeight - oNodeWeight) > 0.000001) {
      return thisNodeWeight < oNodeWeight;
    }

    // if these nodes are have similar weight then return the node with less
    // used space as the lesser node.
    return stat.getScmUsed().isLess(o.getScmUsed().get());
  }

  /**
   * Returns true if the object has same values. Because of issues with
   * equals, and loss of type information this interface supports isEqual.
   *
   * @param o object to compare.
   * @return True, if the values match.
   * TODO : Consider if it makes sense to add remaining to this equation.
   */
  @Override
  public boolean isEqual(SCMNodeStat o) {
    float thisNodeWeight = stat.getScmUsed().get() / (float)
        stat.getCapacity().get();
    float oNodeWeight = o.getScmUsed().get() / (float) o.getCapacity().get();
    return Math.abs(thisNodeWeight - oNodeWeight) < 0.000001;
  }

  /**
   * A resourceCheck, defined by resourceNeeded.
   * For example, S could be bytes required
   * and DatanodeMetric can reply by saying it can be met or not.
   *
   * @param resourceNeeded -  ResourceNeeded in its own metric.
   * @return boolean, True if this resource requirement can be met.
   */
  @Override
  public boolean hasResources(Long resourceNeeded) {
    return false;
  }

  /**
   * Returns the metric.
   *
   * @return T, the object that represents this metric.
   */
  @Override
  public SCMNodeStat get() {
    return stat;
  }

  /**
   * Sets the value of this metric.
   *
   * @param value - value of the metric.
   */
  @Override
  public void set(SCMNodeStat value) {
    stat.set(value.getCapacity().get(), value.getScmUsed().get(),
        value.getRemaining().get(), value.getCommitted().get(),
        value.getFreeSpaceToSpare().get(), value.getReserved().get());
  }

  /**
   * Adds a value of to the base.
   *
   * @param value - value
   */
  @Override
  public void add(SCMNodeStat value) {
    stat.add(value);
  }

  /**
   * subtract a value.
   *
   * @param value value
   */
  @Override
  public void subtract(SCMNodeStat value) {
    stat.subtract(value);
  }

  /**
   * Compares this object with the specified object for order.  Returns a
   * negative integer, zero, or a positive integer as this object is less
   * than, equal to, or greater than the specified object.
   *
   * @param o the object to be compared.
   * @return a negative integer, zero, or a positive integer as this object is
   * less than, equal to, or greater than the specified object.
   * @throws NullPointerException if the specified object is null
   * @throws ClassCastException   if the specified object's type prevents it
   *                              from being compared to this object.
   */
  @Override
  public int compareTo(SCMNodeMetric o) {
    if (isEqual(o.get())) {
      return 0;
    }
    if (isGreater(o.get())) {
      return 1;
    } else {
      return -1;
    }
  }

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

    SCMNodeMetric that = (SCMNodeMetric) o;

    return stat != null ? stat.equals(that.stat) : that.stat == null;
  }

  @Override
  public int hashCode() {
    return stat != null ? stat.hashCode() : 0;
  }

  @Override
  public String toString() {
    return "SCMNodeMetric{" + stat.toString() + '}';
  }
}
