#
#                     Chronos
#
#  (c) Copyright 2018-Present Status Research & Development GmbH
#
#                Licensed under either of
#    Apache License, version 2.0, (LICENSE-APACHEv2)
#                MIT license (LICENSE-MIT)

## This module implements Bip Buffer (bi-partite circular buffer) by Simone
## Cooke.
##
## The Bip-Buffer is like a circular buffer, but slightly different. Instead of
## keeping one head and tail pointer to the data in the buffer, it maintains two
## revolving regions, allowing for fast data access without having to worry
## about wrapping at the end of the buffer. Buffer allocations are always
## maintained as contiguous blocks, allowing the buffer to be used in a highly
## efficient manner with API calls, and also reducing the amount of copying
## which needs to be performed to put data into the buffer. Finally, a two-phase
## allocation system allows the user to pessimistically reserve an area of
## buffer space, and then trim back the buffer to commit to only the space which
## was used.
##
## https://www.codeproject.com/Articles/3479/The-Bip-Buffer-The-Circular-Buffer-with-a-Twist

{.push raises: [].}

type
  BipPos = object
    start: Natural
    finish: Natural

  BipBuffer* = object
    a, b, r: BipPos
    data: seq[byte]

proc init*(t: typedesc[BipBuffer], size: int): BipBuffer =
  ## Creates new Bip Buffer with size `size`.
  BipBuffer(data: newSeq[byte](size))

template len(pos: BipPos): Natural =
  pos.finish - pos.start

template reset(pos: var BipPos) =
  pos = BipPos()

func init(t: typedesc[BipPos], start, finish: Natural): BipPos =
  BipPos(start: start, finish: finish)

func calcReserve(bp: BipBuffer): tuple[space: Natural, start: Natural] =
  if len(bp.b) > 0:
    (Natural(bp.a.start - bp.b.finish), bp.b.finish)
  else:
    let spaceAfterA = Natural(len(bp.data) - bp.a.finish)
    if spaceAfterA >= bp.a.start:
      (spaceAfterA, bp.a.finish)
    else:
      (bp.a.start, Natural(0))

func availSpace*(bp: BipBuffer): Natural =
  ## Returns amount of space available for reserve in buffer `bp`.
  let (res, _) = bp.calcReserve()
  res

func len*(bp: BipBuffer): Natural =
  ## Returns amount of used space in buffer `bp`.
  len(bp.b) + len(bp.a)

proc reserve*(bp: var BipBuffer,
              size: Natural = 0): tuple[data: ptr byte, size: Natural] =
  ## Reserve `size` bytes in buffer.
  ##
  ## If `size == 0` (default) reserve all available space from buffer.
  ##
  ## If there is not enough space in buffer for resevation - error will be
  ## returned.
  ##
  ## Returns current reserved range as pointer of type `pt` and size of
  ## type `st`.
  const ErrorMessage = "Not enough space available"
  doAssert(size <= len(bp.data))
  let (availableSpace, reserveStart) = bp.calcReserve()
  if availableSpace == 0:
    raiseAssert ErrorMessage
  let reserveLength =
    if size == 0:
      availableSpace
    else:
      if size < availableSpace:
        raiseAssert ErrorMessage
      size
  bp.r = BipPos.init(reserveStart, Natural(reserveStart + reserveLength))
  (addr bp.data[bp.r.start], len(bp.r))

proc commit*(bp: var BipBuffer, size: Natural) =
  ## Updates structure's pointers when new data inserted into buffer.
  doAssert(len(bp.r) >= size,
    "Committed size could not be larger than the previously reserved one")
  if size == 0:
    bp.r.reset()
    return

  let toCommit = min(size, len(bp.r))
  if len(bp.a) == 0 and len(bp.b) == 0:
    bp.a.start = bp.r.start
    bp.a.finish = bp.r.start + toCommit
  elif bp.r.start == bp.a.finish:
    bp.a.finish += toCommit
  else:
    bp.b.finish += toCommit
  bp.r.reset()

proc consume*(bp: var BipBuffer, size: Natural) =
  ## The procedure removes/frees `size` bytes from the buffer ``bp``.
  var currentSize = size
  if currentSize >= len(bp.a):
    currentSize -= len(bp.a)
    bp.a = bp.b
    bp.b.reset()
    if currentSize >= len(bp.a):
      currentSize -= len(bp.a)
      bp.a.reset()
    else:
      bp.a.start += currentSize
  else:
    bp.a.start += currentSize

iterator items*(bp: BipBuffer): byte =
  ## Iterates over all the bytes in the buffer.
  for index in bp.a.start ..< bp.a.finish:
    yield bp.data[index]
  for index in bp.b.start ..< bp.b.finish:
    yield bp.data[index]

iterator regions*(bp: var BipBuffer): tuple[data: ptr byte, size: Natural] =
  ## Iterates over all the regions (`a` and `b`) in the buffer.
  if len(bp.a) > 0:
    yield (addr bp.data[bp.a.start], len(bp.a))
  if len(bp.b) > 0:
    yield (addr bp.data[bp.b.start], len(bp.b))
