import { bigIntToBytes, bytesToBigInt, createBytes } from "./bytes.js";

const FIELD_PRIME =
  0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;
const GROUP_ORDER =
  0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const GENERATOR = /** @type {const} */ ({
  x: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
  y: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
});

/**
 * @typedef {Object} Secp256k1Point
 * @property {bigint} x
 * @property {bigint} y
 */

/**
 * @param {bigint} value
 * @param {bigint} modulo
 */
function mod(value, modulo) {
  const result = value % modulo;

  return result >= 0n ? result : result + modulo;
}

/**
 * @param {bigint} value
 * @param {bigint} exponent
 * @param {bigint} modulo
 */
function modPow(value, exponent, modulo) {
  let result = 1n;
  let base = mod(value, modulo);
  let power = exponent;

  while (power > 0n) {
    if (power & 1n) result = mod(result * base, modulo);

    base = mod(base * base, modulo);
    power >>= 1n;
  }

  return result;
}

/**
 * @param {bigint} value
 */
function invertField(value) {
  let low = mod(value, FIELD_PRIME);
  let high = FIELD_PRIME;
  let lowCoefficient = 1n;
  let highCoefficient = 0n;

  while (low > 1n) {
    const ratio = high / low;

    [low, high] = [high - low * ratio, low];
    [lowCoefficient, highCoefficient] = [
      highCoefficient - lowCoefficient * ratio,
      lowCoefficient,
    ];
  }

  return mod(lowCoefficient, FIELD_PRIME);
}

/**
 * @param {Secp256k1Point} point
 */
function isOnCurve(point) {
  const left = mod(point.y * point.y, FIELD_PRIME);
  const right = mod(point.x * point.x * point.x + 7n, FIELD_PRIME);

  return left === right;
}

/**
 * @param {Secp256k1Point} point
 */
function doublePoint(point) {
  if (point.y === 0n) return null;

  const slope = mod(
    3n * point.x * point.x * invertField(2n * point.y),
    FIELD_PRIME,
  );
  const x = mod(slope * slope - 2n * point.x, FIELD_PRIME);
  const y = mod(slope * (point.x - x) - point.y, FIELD_PRIME);

  return { x, y };
}

/**
 * @param {Secp256k1Point} point
 */
function negatePoint(point) {
  return { x: point.x, y: mod(-point.y, FIELD_PRIME) };
}

/**
 * @param {Secp256k1Point} point
 */
function forceEvenY(point) {
  return point.y & 1n ? negatePoint(point) : point;
}

/**
 * @param {Secp256k1Point | null} left
 * @param {Secp256k1Point | null} right
 * @returns {Secp256k1Point | null}
 */
function addPoints(left, right) {
  if (!left) return right;
  if (!right) return left;

  if (left.x === right.x) {
    if (mod(left.y + right.y, FIELD_PRIME) === 0n) return null;

    return doublePoint(left);
  }

  const slope = mod(
    (right.y - left.y) * invertField(right.x - left.x),
    FIELD_PRIME,
  );
  const x = mod(slope * slope - left.x - right.x, FIELD_PRIME);
  const y = mod(slope * (left.x - x) - left.y, FIELD_PRIME);

  return { x, y };
}

/**
 * @param {bigint} scalar
 * @param {Secp256k1Point} point
 * @returns {Secp256k1Point | null}
 */
function multiplyPoint(scalar, point) {
  let result = /** @type {Secp256k1Point | null} */ (null);
  let addend = /** @type {Secp256k1Point | null} */ (point);
  let remaining = scalar;

  while (remaining > 0n) {
    if (remaining & 1n) result = addPoints(result, addend);

    addend = addPoints(addend, addend);
    remaining >>= 1n;
  }

  return result;
}

/**
 * @param {bigint} x
 * @param {boolean} odd
 */
function liftX(x, odd) {
  if (x >= FIELD_PRIME) {
    throw new Error("Invalid secp256k1 x coordinate");
  }

  let y = modPow(x * x * x + 7n, (FIELD_PRIME + 1n) / 4n, FIELD_PRIME);

  if (Boolean(y & 1n) !== odd) {
    y = FIELD_PRIME - y;
  }

  const point = { x, y };

  if (!isOnCurve(point)) {
    throw new Error("Invalid secp256k1 point");
  }

  return point;
}

/**
 * @param {Uint8Array} publicKey
 */
function parseCompressedPublicKey(publicKey) {
  if (
    publicKey.length !== 33 ||
    (publicKey[0] !== 0x02 && publicKey[0] !== 0x03)
  ) {
    throw new Error("Expected a compressed public key");
  }

  return liftX(bytesToBigInt(publicKey.slice(1)), publicKey[0] === 0x03);
}

/**
 * @param {Secp256k1Point} point
 */
function compressPublicKey(point) {
  const publicKey = createBytes(33);

  publicKey[0] = point.y & 1n ? 0x03 : 0x02;
  publicKey.set(bigIntToBytes(point.x, 32), 1);

  return publicKey;
}

/**
 * @param {Uint8Array} publicKey
 */
export function getXOnlyPublicKey(publicKey) {
  const point = forceEvenY(parseCompressedPublicKey(publicKey));

  return bigIntToBytes(point.x, 32);
}

/**
 * @param {Uint8Array} publicKey
 * @param {Uint8Array} tweak
 */
export function addPublicKeyTweak(publicKey, tweak) {
  const scalar = bytesToBigInt(tweak);

  if (scalar === 0n || scalar >= GROUP_ORDER) {
    throw new Error("Invalid secp256k1 public key tweak");
  }

  const tweakPoint = multiplyPoint(scalar, GENERATOR);
  const childPoint = addPoints(parseCompressedPublicKey(publicKey), tweakPoint);

  if (!childPoint) {
    throw new Error("Invalid secp256k1 child public key");
  }

  return compressPublicKey(childPoint);
}

/**
 * @param {Uint8Array} publicKey
 * @param {Uint8Array} tweak
 */
export function addXOnlyPublicKeyTweak(publicKey, tweak) {
  const scalar = bytesToBigInt(tweak);

  if (scalar >= GROUP_ORDER) {
    throw new Error("Invalid secp256k1 x-only public key tweak");
  }

  const internalPoint = forceEvenY(parseCompressedPublicKey(publicKey));
  const tweakPoint = scalar === 0n ? null : multiplyPoint(scalar, GENERATOR);
  const outputPoint = addPoints(internalPoint, tweakPoint);

  if (!outputPoint) {
    throw new Error("Invalid secp256k1 x-only child public key");
  }

  return bigIntToBytes(outputPoint.x, 32);
}
