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

import static org.hyperledger.besu.crypto.altbn128.AltBn128Fq12Point.twist;

import java.math.BigInteger;
import java.util.Arrays;

/**
 * Adapted from the pc_ecc (Apache 2 License) implementation:
 * https://github.com/ethereum/py_ecc/blob/master/py_ecc/bn128/bn128_field_elements.py
 */
public class AltBn128Fq12Pairer {

  private static final int LOG_ATE_LOOP_COUNT = 63;

  private static final BigInteger ATE_LOOP_COUNT = new BigInteger("29793968203157093288");

  private static final BigInteger CURVE_ORDER =
      new BigInteger(
          "21888242871839275222246405745257275088548364400416034343698204186575808495617");

  /**
   * Pair fq 12.
   *
   * @param p the p
   * @param q the q
   * @return the fq 12
   */
  public static Fq12 pair(final AltBn128Point p, final AltBn128Fq2Point q) {
    return millerLoop(cast(p), twist(q));
  }

  private static AltBn128Fq12Point cast(final AltBn128Point p) {
    final Fq[] newX = new Fq[Fq12.DEGREE];
    Arrays.fill(newX, Fq.zero());
    newX[0] = p.getX();
    final Fq[] newY = new Fq[Fq12.DEGREE];
    Arrays.fill(newY, Fq.zero());
    newY[0] = p.getY();

    return new AltBn128Fq12Point(new Fq12(newX), new Fq12(newY));
  }

  private static Fq12 millerLoop(final AltBn128Fq12Point p, final AltBn128Fq12Point q) {
    if (p.isInfinity() || q.isInfinity()) {
      return Fq12.one();
    }

    AltBn128Fq12Point r = q;
    Fq12 f = Fq12.one();
    for (int i = LOG_ATE_LOOP_COUNT; i >= 0; --i) {
      f = f.multiply(f).multiply(lineFunc(r, r, p));
      r = r.doub();
      if (ATE_LOOP_COUNT.testBit(i)) {
        f = f.multiply(lineFunc(r, q, p));
        r = r.add(q);
      }
    }

    final AltBn128Fq12Point q1 =
        new AltBn128Fq12Point(
            q.getX().power(FieldElement.FIELD_MODULUS), q.getY().power(FieldElement.FIELD_MODULUS));
    final AltBn128Fq12Point nQ2 =
        new AltBn128Fq12Point(
            q1.getX().power(FieldElement.FIELD_MODULUS),
            q1.getY().negate().power(FieldElement.FIELD_MODULUS));
    f = f.multiply(lineFunc(r, q1, p));
    r = r.add(q1);
    f = f.multiply(lineFunc(r, nQ2, p));

    return f;
  }

  /**
   * Finalize fq 12.
   *
   * @param f the f
   * @return the fq 12
   */
  public static Fq12 finalize(final Fq12 f) {
    return f.power(FieldElement.FIELD_MODULUS.pow(12).subtract(BigInteger.ONE).divide(CURVE_ORDER));
  }

  private static Fq12 lineFunc(
      final AltBn128Fq12Point p1, final AltBn128Fq12Point p2, final AltBn128Fq12Point t) {
    final Fq12 x1 = p1.getX();
    final Fq12 y1 = p1.getY();
    final Fq12 x2 = p2.getX();
    final Fq12 y2 = p2.getY();
    final Fq12 xT = t.getX();
    final Fq12 yT = t.getY();

    if (!x1.equals(x2)) {
      final Fq12 m = y2.subtract(y1).divide(x2.subtract(x1));
      final Fq12 result = m.multiply(xT.subtract(x1)).subtract(yT.subtract(y1));
      return result;
    } else if (y1.equals(y2)) {
      final Fq12 m = x1.power(2).multiply(3).divide(y1.multiply(2));
      final Fq12 result = m.multiply(xT.subtract(x1)).subtract(yT.subtract(y1));
      return result;
    } else {
      return xT.subtract(x1);
    }
  }
}
