/*
 * Copyright ConsenSys AG.
 *
 * Licensed 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.
 *
 * SPDX-License-Identifier: Apache-2.0
 */
package org.hyperledger.besu.ethereum.mainnet;

import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.Quantity;

import java.math.BigInteger;

import com.google.common.primitives.Ints;

public abstract class ClassicDifficultyCalculators {
  private static final BigInteger MINIMUM_DIFFICULTY = BigInteger.valueOf(131_072L);
  private static final BigInteger DIFFICULTY_BOUND_DIVISOR = BigInteger.valueOf(2_048L);
  private static final BigInteger BIGINT_2 = BigInteger.valueOf(2L);
  private static final long EXPONENTIAL_DIFF_PERIOD = 100_000L;
  private static final long PAUSE_BLOCK = 3_000_000L;
  private static final long FIXED_DIFF = PAUSE_BLOCK / EXPONENTIAL_DIFF_PERIOD;
  private static final long CONTINUE_BLOCK = 5_000_000L;
  private static final long DELAY = (CONTINUE_BLOCK - PAUSE_BLOCK) / EXPONENTIAL_DIFF_PERIOD;

  public static DifficultyCalculator DIFFICULTY_BOMB_PAUSED =
      (time, parent) -> {
        final BigInteger parentDifficulty = difficulty(parent.getDifficulty());
        final BigInteger difficulty =
            ensureMinimumDifficulty(
                BigInteger.valueOf(Math.max(1 - (time - parent.getTimestamp()) / 10, -99L))
                    .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR))
                    .add(parentDifficulty));
        return adjustForDifficultyPause(FIXED_DIFF, difficulty);
      };

  public static DifficultyCalculator DIFFICULTY_BOMB_DELAYED =
      (time, parent) -> {
        final BigInteger parentDifficulty = difficulty(parent.getDifficulty());
        final BigInteger difficulty =
            ensureMinimumDifficulty(
                BigInteger.valueOf(Math.max(1 - (time - parent.getTimestamp()) / 10, -99L))
                    .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR))
                    .add(parentDifficulty));
        final long periodCount = (parent.getNumber() + 1) / EXPONENTIAL_DIFF_PERIOD;
        return adjustForDifficultyDelay(periodCount, difficulty);
      };

  public static DifficultyCalculator DIFFICULTY_BOMB_REMOVED =
      (time, parent) -> {
        final BigInteger parentDifficulty = difficulty(parent.getDifficulty());
        final BigInteger difficulty =
            ensureMinimumDifficulty(
                BigInteger.valueOf(Math.max(1 - (time - parent.getTimestamp()) / 10, -99L))
                    .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR))
                    .add(parentDifficulty));
        return difficulty;
      };

  public static DifficultyCalculator EIP100 =
      (time, parent) -> {
        final BigInteger parentDifficulty = difficulty(parent.getDifficulty());
        final boolean hasOmmers = !parent.getOmmersHash().equals(Hash.EMPTY_LIST_HASH);
        final BigInteger difficulty =
            ensureMinimumDifficulty(
                BigInteger.valueOf(byzantiumX(time, parent.getTimestamp(), hasOmmers))
                    .multiply(parentDifficulty.divide(DIFFICULTY_BOUND_DIVISOR))
                    .add(parentDifficulty));
        return difficulty;
      };

  private static long byzantiumX(
      final long blockTime, final long parentTime, final boolean hasOmmers) {
    long x = (blockTime - parentTime) / 9L;
    if (hasOmmers) {
      x = 2 - x;
    } else {
      x = 1 - x;
    }
    return Math.max(x, -99L);
  }

  private static BigInteger adjustForDifficultyDelay(
      final long periodCount, final BigInteger difficulty) {
    return difficulty.add(BIGINT_2.pow(Ints.checkedCast(periodCount - DELAY - 2)));
  }

  private static BigInteger adjustForDifficultyPause(
      final long periodCount, final BigInteger difficulty) {
    return difficulty.add(BIGINT_2.pow(Ints.checkedCast(periodCount - 2)));
  }

  private static BigInteger ensureMinimumDifficulty(final BigInteger difficulty) {
    return difficulty.compareTo(MINIMUM_DIFFICULTY) < 0 ? MINIMUM_DIFFICULTY : difficulty;
  }

  private static BigInteger difficulty(final Quantity value) {
    return value.getAsBigInteger();
  }
}
