///|
#cfg(target="native")
priv suberror X25519Error {
  X25519BadScalarLength
  X25519BadPointLength
} derive(Debug, ToJson)

///|
#cfg(target="native")
let x25519_121665 : FixedArray[Int64] = {
  let out = FixedArray::make(16, 0L)
  out[0] = 0xdb41L
  out[1] = 1L
  out
}

///|
#cfg(target="native")
fn x25519_gf(
  init? : FixedArray[Int64] = FixedArray::make(16, 0L),
) -> FixedArray[Int64] {
  let out = FixedArray::make(16, 0L)
  for i in 0..<16 {
    out[i] = init[i]
  }
  out
}

///|
#cfg(target="native")
fn x25519_carry(o : FixedArray[Int64]) -> Unit {
  let mut carry = 1L
  for i in 0..<16 {
    let value = o[i] + carry + 65535L
    carry = value >> 16
    o[i] = value - carry * 65536L
  }
  o[0] += 38L * (carry - 1L)
}

///|
#cfg(target="native")
fn x25519_select(
  p : FixedArray[Int64],
  q : FixedArray[Int64],
  bit : Int64,
) -> Unit {
  let mask = (bit - 1L).lnot()
  for i in 0..<16 {
    let t = mask & (p[i] ^ q[i])
    p[i] = p[i] ^ t
    q[i] = q[i] ^ t
  }
}

///|
#cfg(target="native")
fn x25519_add(
  o : FixedArray[Int64],
  a : FixedArray[Int64],
  b : FixedArray[Int64],
) -> Unit {
  for i in 0..<16 {
    o[i] = a[i] + b[i]
  }
}

///|
#cfg(target="native")
fn x25519_sub(
  o : FixedArray[Int64],
  a : FixedArray[Int64],
  b : FixedArray[Int64],
) -> Unit {
  for i in 0..<16 {
    o[i] = a[i] - b[i]
  }
}

///|
#cfg(target="native")
fn x25519_mul(
  o : FixedArray[Int64],
  a : FixedArray[Int64],
  b : FixedArray[Int64],
) -> Unit {
  let t = FixedArray::make(31, 0L)
  for i in 0..<16 {
    for j in 0..<16 {
      t[i + j] += a[i] * b[j]
    }
  }
  for i in 0..<15 {
    t[i] += 38L * t[i + 16]
  }
  for i in 0..<16 {
    o[i] = t[i]
  }
  x25519_carry(o)
  x25519_carry(o)
}

///|
#cfg(target="native")
fn x25519_square(o : FixedArray[Int64], a : FixedArray[Int64]) -> Unit {
  x25519_mul(o, a, a)
}

///|
#cfg(target="native")
fn x25519_inverse(o : FixedArray[Int64], i : FixedArray[Int64]) -> Unit {
  let c = x25519_gf(init=i)
  for a = 253; a >= 0; a = a - 1 {
    x25519_square(c, c)
    if a != 2 && a != 4 {
      x25519_mul(c, c, i)
    }
  }
  for a in 0..<16 {
    o[a] = c[a]
  }
}

///|
#cfg(target="native")
fn x25519_pack(n : FixedArray[Int64]) -> Bytes {
  let t = x25519_gf(init=n)
  let m = FixedArray::make(16, 0L)
  for _ in 0..<3 {
    x25519_carry(t)
  }
  for _ in 0..<2 {
    m[0] = t[0] - 0xffedL
    for i in 1..<15 {
      m[i] = t[i] - 0xffffL - ((m[i - 1] >> 16) & 1L)
      m[i - 1] = m[i - 1] & 0xffffL
    }
    m[15] = t[15] - 0x7fffL - ((m[14] >> 16) & 1L)
    let b = (m[15] >> 16) & 1L
    m[14] = m[14] & 0xffffL
    x25519_select(t, m, 1L - b)
  }
  let out = @buffer.new()
  for i in 0..<16 {
    out.write_byte((t[i] & 0xffL).to_byte())
    out.write_byte(((t[i] >> 8) & 0xffL).to_byte())
  }
  out.contents()
}

///|
#cfg(target="native")
fn x25519_unpack(point : Bytes) -> FixedArray[Int64] raise {
  guard point.length() == 32 else { raise X25519BadPointLength }
  let out = FixedArray::make(16, 0L)
  for i in 0..<16 {
    out[i] = point[2 * i].to_int64() + (point[2 * i + 1].to_int64() << 8)
  }
  out[15] = out[15] & 0x7fffL
  out
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn x25519(scalar : Bytes, point : Bytes) -> Bytes raise {
  guard scalar.length() == 32 else { raise X25519BadScalarLength }
  guard point.length() == 32 else { raise X25519BadPointLength }
  let z = FixedArray::make(32, b'\x00')
  for i in 0..<32 {
    z[i] = scalar[i]
  }
  z[0] = z[0] & b'\xf8'
  z[31] = (z[31] & b'\x7f') | b'\x40'
  let x = x25519_unpack(point)
  let a = x25519_gf()
  let b = x25519_gf()
  let c = x25519_gf()
  let d = x25519_gf()
  let e = x25519_gf()
  let f = x25519_gf()
  for i in 0..<16 {
    b[i] = x[i]
  }
  a[0] = 1L
  d[0] = 1L
  for pos = 254; pos >= 0; pos = pos - 1 {
    let bit = ((z[pos >> 3].to_int() >> (pos & 7)) & 1).to_int64()
    x25519_select(a, b, bit)
    x25519_select(c, d, bit)
    x25519_add(e, a, c)
    x25519_sub(a, a, c)
    x25519_add(c, b, d)
    x25519_sub(b, b, d)
    x25519_square(d, e)
    x25519_square(f, a)
    x25519_mul(a, c, a)
    x25519_mul(c, b, e)
    x25519_add(e, a, c)
    x25519_sub(a, a, c)
    x25519_square(b, a)
    x25519_sub(c, d, f)
    x25519_mul(a, c, x25519_121665)
    x25519_add(a, a, d)
    x25519_mul(c, c, a)
    x25519_mul(a, d, f)
    x25519_mul(d, b, x)
    x25519_square(b, e)
    x25519_select(a, b, bit)
    x25519_select(c, d, bit)
  }
  x25519_inverse(c, c)
  x25519_mul(a, a, c)
  x25519_pack(a)
}

///|
#cfg(target="native")
#warnings("-unused_value")
fn x25519_base(scalar : Bytes) -> Bytes raise {
  let base = FixedArray::make(32, b'\x00')
  base[0] = b'\x09'
  x25519(scalar, base.unsafe_reinterpret_as_bytes())
}