# Nim-LibP2P
# Copyright (c) 2022 Status Research & Development GmbH
# Licensed under either of
#  * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
#  * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.

## This module implementes API for libp2p peer.

when (NimMajor, NimMinor) < (1, 4):
  {.push raises: [Defect].}
else:
  {.push raises: [].}
{.push public.}

import
  std/[hashes, strutils],
  stew/[base58, results],
  chronicles,
  nimcrypto/utils,
  utility,
  ./crypto/crypto, ./multicodec, ./multihash, ./vbuffer,
  ./protobuf/minprotobuf

export results, utility

const
  maxInlineKeyLength* = 42

type
  PeerId* = object
    data*: seq[byte]

func `$`*(pid: PeerId): string =
  ## Return base58 encoded ``pid`` representation.
  # This unusual call syntax is used to avoid a strange Nim compilation error
  base58.encode(Base58, pid.data)

func shortLog*(pid: PeerId): string =
  ## Returns compact string representation of ``pid``.
  var spid = $pid
  if len(spid) > 10:
    spid[3] = '*'

    when (NimMajor, NimMinor) > (1, 4):
      spid.delete(4 .. spid.high - 6)
    else:
      spid.delete(4, spid.high - 6)

  spid

chronicles.formatIt(PeerId): shortLog(it)

func toBytes*(pid: PeerId, data: var openArray[byte]): int =
  ## Store PeerId ``pid`` to array of bytes ``data``.
  ##
  ## Returns number of bytes needed to store ``pid``.
  result = len(pid.data)
  if len(data) >= result and result > 0:
    copyMem(addr data[0], unsafeAddr pid.data[0], result)

template getBytes*(pid: PeerId): seq[byte] =
  ## Return PeerId ``pid`` as array of bytes.
  pid.data

func hex*(pid: PeerId): string =
  ## Returns hexadecimal string representation of ``pid``.
  toHex(pid.data)

template len*(pid: PeerId): int =
  ## Returns length of ``pid`` binary representation.
  len(pid.data)

func cmp*(a, b: PeerId): int =
  ## Compares two peer ids ``a`` and ``b``.
  ## Returns:
  ##
  ## | 0 iff a == b
  ## | < 0 iff a < b
  ## | > 0 iff a > b
  var i = 0
  var m = min(len(a.data), len(b.data))
  while i < m:
    result = ord(a.data[i]) - ord(b.data[i])
    if result != 0: return
    inc(i)
  result = len(a.data) - len(b.data)

template `<=`*(a, b: PeerId): bool =
  (cmp(a, b) <= 0)

template `<`*(a, b: PeerId): bool =
  (cmp(a, b) < 0)

template `>=`*(a, b: PeerId): bool =
  (cmp(a, b) >= 0)

template `>`*(a, b: PeerId): bool =
  (cmp(a, b) > 0)

template `==`*(a, b: PeerId): bool =
  (cmp(a, b) == 0)

template hash*(pid: PeerId): Hash =
  hash(pid.data)

func validate*(pid: PeerId): bool =
  ## Validate check if ``pid`` is empty or not.
  len(pid.data) > 0 and MultiHash.validate(pid.data)

func hasPublicKey*(pid: PeerId): bool =
  ## Returns ``true`` if ``pid`` is small enough to hold public key inside.
  if len(pid.data) > 0:
    var mh: MultiHash
    if MultiHash.decode(pid.data, mh).isOk:
      if mh.mcodec == multiCodec("identity"):
        result = true

func extractPublicKey*(pid: PeerId, pubkey: var PublicKey): bool =
  ## Returns ``true`` if public key was successfully decoded from PeerId
  ## ``pid``and stored to ``pubkey``.
  ##
  ## Returns ``false`` otherwise.
  if len(pid.data) > 0:
    var mh: MultiHash
    if MultiHash.decode(pid.data, mh).isOk:
      if mh.mcodec == multiCodec("identity"):
        let length = len(mh.data.buffer)
        result = pubkey.init(mh.data.buffer.toOpenArray(mh.dpos, length - 1))

func init*(pid: var PeerId, data: openArray[byte]): bool =
  ## Initialize peer id from raw binary representation ``data``.
  ##
  ## Returns ``true`` if peer was successfully initialiazed.
  var p = PeerId(data: @data)
  if p.validate():
    pid = p
    result = true

func init*(pid: var PeerId, data: string): bool =
  ## Initialize peer id from base58 encoded string representation.
  ##
  ## Returns ``true`` if peer was successfully initialiazed.
  var p = newSeq[byte](len(data) + 4)
  var length = 0
  if Base58.decode(data, p, length) == Base58Status.Success:
    p.setLen(length)
    var opid: PeerId
    opid.data = p
    if opid.validate():
      pid = opid
      result = true

func init*(t: typedesc[PeerId], data: openArray[byte]): Result[PeerId, cstring] =
  ## Create new peer id from raw binary representation ``data``.
  var res: PeerId
  if not init(res, data):
    err("peerid: incorrect PeerId binary form")
  else:
    ok(res)

func init*(t: typedesc[PeerId], data: string): Result[PeerId, cstring] =
  ## Create new peer id from base58 encoded string representation ``data``.
  var res: PeerId
  if not init(res, data):
    err("peerid: incorrect PeerId string")
  else:
    ok(res)

func init*(t: typedesc[PeerId], pubkey: PublicKey): Result[PeerId, cstring] =
  ## Create new peer id from public key ``pubkey``.
  var pubraw = ? pubkey.getBytes().orError(
    cstring("peerid: failed to get bytes from given key"))
  var mh: MultiHash
  if len(pubraw) <= maxInlineKeyLength:
    mh = ? MultiHash.digest("identity", pubraw)
  else:
    mh = ? MultiHash.digest("sha2-256", pubraw)
  ok(PeerId(data: mh.data.buffer))

func init*(t: typedesc[PeerId], seckey: PrivateKey): Result[PeerId, cstring] =
  ## Create new peer id from private key ``seckey``.
  PeerId.init(? seckey.getPublicKey().orError(cstring("invalid private key")))

proc random*(t: typedesc[PeerId], rng = newRng()): Result[PeerId, cstring] =
  ## Create new peer id with random public key.
  let randomKey = PrivateKey.random(Secp256k1, rng[])[]
  PeerId.init(randomKey).orError(cstring("failed to generate random key"))

func match*(pid: PeerId, pubkey: PublicKey): bool =
  ## Returns ``true`` if ``pid`` matches public key ``pubkey``.
  let p = PeerId.init(pubkey)
  if p.isErr:
    false
  else:
    pid == p.get()

func match*(pid: PeerId, seckey: PrivateKey): bool =
  ## Returns ``true`` if ``pid`` matches private key ``seckey``.
  let p = PeerId.init(seckey)
  if p.isErr:
    false
  else:
    pid == p.get()

## Serialization/Deserialization helpers

func write*(vb: var VBuffer, pid: PeerId) =
  ## Write PeerId value ``peerid`` to buffer ``vb``.
  vb.writeSeq(pid.data)

func write*(pb: var ProtoBuffer, field: int, pid: PeerId) =
  ## Write PeerId value ``peerid`` to object ``pb`` using ProtoBuf's encoding.
  write(pb, field, pid.data)

func getField*(pb: ProtoBuffer, field: int,
               pid: var PeerId): ProtoResult[bool] {.inline.} =
  ## Read ``PeerId`` from ProtoBuf's message and validate it
  var buffer: seq[byte]
  let res = ? pb.getField(field, buffer)
  if not(res):
    ok(false)
  else:
    var peerId: PeerId
    if peerId.init(buffer):
      pid = peerId
      ok(true)
    else:
      err(ProtoError.IncorrectBlob)