// This file is based on the Go implementation found here:
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.1:src/compress/flate/deflatefast.go
// which has the copyright notice:
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// This encoding algorithm, which prioritizes speed over output size, is
// based on Snappy's LZ77-style encoder: github.com/golang/snappy

///|
let table_bits = 14 // Bits used in the table.

///|
let table_size : Int = 1 << table_bits // Size of the table.

///|
let table_mask : Int = table_size - 1 // Mask for table indices. Redundant, but can eliminate bounds checks.

///|
let table_shift : Int = 32 - table_bits // Right-shift to get the table_bits most significant bits of a UInt.

// The LZ77 step produces a sequence of literal tokens and 
// pair tokens. The offset is also known as distance. The underlying wire
// format limits the range of lengths and offsets. For example, there are
// 256 legitimate lengths: those in the range [3, 258]. This package's
// compressor uses a higher minimum match length, enabling optimizations
// such as finding matches via 32-bit loads and compares.

///|
let base_match_length = 3 // The smallest match length per the RFC section 3.2.5

///|
let max_match_length = 258 // The largest match length

///|
let base_match_offset = 1 // The smallest match offset

///|
let max_match_offset : Int = 1 << 15 // The largest match offset

// The maximum number of tokens we put into a single flate block, just to
// stop things from getting too large.

///|
let max_store_block_size = 65535

// Reset the buffer offset when reaching this.
// Offsets are stored between blocks as Int values.
// Since the offset we are checking against is at the beginning
// of the buffer, we need to subtract the current and input
// buffer to not risk overflowing the Int.

///|
let buffer_reset : Int = @int.MAX_VALUE - max_store_block_size * 2

///|
fn load32(b : Slice[Byte], i : Int) -> UInt {
  return b[i + 0].to_uint() |
    (b[i + 1].to_uint() << 8) |
    (b[i + 2].to_uint() << 16) |
    (b[i + 3].to_uint() << 24)
}

///|
fn load64(b : Slice[Byte], i : Int) -> UInt64 {
  return b[i + 0].to_uint().to_uint64() |
    (b[i + 1].to_uint().to_uint64() << 8) |
    (b[i + 2].to_uint().to_uint64() << 16) |
    (b[i + 3].to_uint().to_uint64() << 24) |
    (b[i + 4].to_uint().to_uint64() << 32) |
    (b[i + 5].to_uint().to_uint64() << 40) |
    (b[i + 6].to_uint().to_uint64() << 48) |
    (b[i + 7].to_uint().to_uint64() << 56)
}

///|
fn hash(u : UInt) -> Int { // uint32 - but always used as int below
  let v = (u * 0x1e35a7bd) >> table_shift
  return v.reinterpret_as_int()
}

// These constants are defined by the Snappy implementation so that its
// assembly implementation can fast-path some 16-bytes-at-a-time copies. They
// aren't necessary in the pure Go implementation, as we don't use those same
// optimizations, but using the same thresholds doesn't really hurt.

///|
let input_margin : Int = 16 - 1

///|
let min_non_literal_block_size : Int = 1 + 1 + input_margin

///|
priv struct TableEntry {
  val : UInt // Value at destination
  mut offset : Int
}

// DeflateFast maintains the table for matches,
// and the previous byte block for cross block matching.

///|
priv struct DeflateFast {
  table : FixedArray[TableEntry] // [table_size]TableEntry
  mut prev : Slice[Byte] // Previous block, zero length if unknown.
  mut cur : Int // Current match offset.
}

///|
fn DeflateFast::new() -> DeflateFast {
  {
    table: FixedArray::make(table_size, { val: 0U, offset: 0 }),
    prev: Slice::new([]),
    cur: max_store_block_size,
  }
}

// encode encodes a block given in src and appends tokens
// to dst and returns the result.

///|
fn DeflateFast::encode(
  self : DeflateFast,
  dst : Array[Token],
  src : Slice[Byte],
) -> Array[Token] {
  let mut dst = dst
  // Ensure that self.cur doesn't wrap.
  if self.cur >= buffer_reset {
    self.shift_offsets()
  }

  // This check isn't in the Snappy implementation, but there, the caller
  // instead of the callee handles this case.
  if src.length() < min_non_literal_block_size {
    self.cur += max_store_block_size
    self.prev = Slice::new([])
    return emit_literal(dst, src)
  }

  // s_limit is when to stop looking for offset/length copies. The input_margin
  // lets us use a fast path for emit_literal in the main loop, while we are
  // looking for copies.
  let s_limit = src.length() - input_margin
  let mut next_emit = 0
  let mut s = 0

  // next_emit is where in src the next emit_literal should start from.
  let mut cv = load32(src, s)
  let mut next_hash = hash(cv)
  let emit_remainder = fn() {
    if next_emit < src.length() {
      dst = emit_literal(dst, src[next_emit:])
    }
    self.cur += src.length()
    let _ = slice_copy(self.prev, src)
    return dst
  }

  //
  for ;; {
    // Copied from the C++ snappy implementation:
    //
    // Heuristic match skipping: If 32 bytes are scanned with no matches
    // found, start looking only at every other byte. If 32 more bytes are
    // scanned (or skipped), look at every third byte, etc.. When a match
    // is found, immediately go back to looking at every byte. This is a
    // small loss (~5% performance, ~0.1% density) for compressible data
    // due to more bookkeeping, but for non-compressible data (such as
    // JPEG) it's a huge win since the compressor quickly "realizes" the
    // data is incompressible and doesn't bother looking for matches
    // everywhere.
    //
    // The "skip" variable keeps track of how many bytes there are since
    // the last match; dividing it by 32 (ie. right-shifting by five) gives
    // the number of bytes to move ahead for each iteration.
    let mut skip = 32
    let mut next_s = s
    let mut candidate : TableEntry = { val: 0U, offset: 0 }

    //
    for ;; {
      s = next_s
      let bytes_between_hash_lookups = skip >> 5
      next_s = s + bytes_between_hash_lookups
      skip += bytes_between_hash_lookups
      if next_s > s_limit {
        return emit_remainder()
      }
      candidate = self.table[next_hash & table_mask]
      let now = load32(src, next_s)
      self.table[next_hash & table_mask] = { offset: s + self.cur, val: cv }
      next_hash = hash(now)
      let offset = s - (candidate.offset - self.cur)
      if offset > max_match_offset || cv != candidate.val {
        // Out of range or not matched.
        cv = now
        continue
      }
      break
    }

    // A 4-byte match has been found. We'll later see if more than 4 bytes
    // match. But, prior to the match, src[next_emit:s] are unmatched. Emit
    // them as literal bytes.
    dst = emit_literal(dst, src[next_emit:s])

    // Call emitCopy, and then see if another emitCopy could be our next
    // move. Repeat until we find no match for the input immediately after
    // what was consumed by the last emitCopy call.
    //
    // If we exit this loop normally then we need to call emit_literal next,
    // though we don't yet know how big the literal will be. We handle that
    // by proceeding to the next iteration of the main loop. We also can
    // exit this loop via goto if we get close to exhausting the input.
    for ;; {
      // Invariant: we have a 4-byte match at s, and no need to emit any
      // literal bytes prior to s.

      // Extend the 4-byte match as long as possible.
      //
      s += 4
      let t = candidate.offset - self.cur + 4
      let l = self.match_len(s, t, src)

      // match_token is flate's equivalent of Snappy's emitCopy. (length,offset)
      dst.push(
        match_token(
          (l + 4 - base_match_length).reinterpret_as_uint(),
          (s - t - base_match_offset).reinterpret_as_uint(),
        ),
      )
      s += l
      next_emit = s
      if s >= s_limit {
        return emit_remainder()
      }

      // We could immediately start working at s now, but to improve
      // compression we first update the hash table at s-1 and at s. If
      // another emitCopy is not our next move, also calculate next_hash
      // at s+1. At least on GOARCH=amd64, these three hash calculations
      // are faster as one load64 call (with some shifts) instead of
      // three load32 calls.
      let mut x = load64(src, s - 1)
      let prev_hash = hash(x.to_uint())
      self.table[prev_hash & table_mask] = {
        offset: self.cur + s - 1,
        val: x.to_uint(),
      }
      x = x >> 8
      let curr_hash = hash(x.to_uint())
      candidate = self.table[curr_hash & table_mask]
      self.table[curr_hash & table_mask] = {
        offset: self.cur + s,
        val: x.to_uint(),
      }
      let offset = s - (candidate.offset - self.cur)
      if offset > max_match_offset || x.to_uint() != candidate.val {
        cv = (x >> 8).to_uint()
        next_hash = hash(cv)
        s += 1
        break
      }
    }
  }
  //
  emit_remainder()
}

///|
fn emit_literal(dst : Array[Token], lit : Slice[Byte]) -> Array[Token] {
  for i in 0.. Int {
  let mut s1 = s + max_match_length - 4
  if s1 > src.length() {
    s1 = src.length()
  }

  // If we are inside the current block
  if t >= 0 {
    let a_length = s1 - s
    // Extend the match to be as long as possible.
    for i = 0; i < a_length; i = i + 1 {
      if src[s + i] != src[t + i] {
        return i
      }
    }
    return a_length
  }

  // We found a match in the previous block.
  let tp = self.prev.length() + t
  if tp < 0 {
    return 0
  }

  // Extend the match to be as long as possible.
  let a_length = s1 - s
  let mut b_length = self.prev.length() - tp
  if b_length > a_length {
    b_length = a_length
  }
  for i = 0; i < b_length; i = i + 1 {
    if src[s + i] != self.prev[tp + i] {
      return i
    }
  }

  // If we reached our limit, we matched everything we are
  // allowed to in the previous block and we return.
  let n = b_length
  if s + n == s1 {
    return n
  }

  // Continue looking for more matches in the current block.
  let a_length = s1 - (s + n)
  for i = 0; i < a_length; i = i + 1 {
    if src[s + n + i] != src[i] {
      return i + n
    }
  }
  return a_length + n
}

// Reset resets the encoding history.
// This ensures that no matches are made to the previous block.

///|
fn DeflateFast::reset(self : DeflateFast) -> Unit {
  self.prev = Slice::new([])
  // Bump the offset, so all matches will fail distance check.
  // Nothing should be >= self.cur in the table.
  self.cur += max_match_offset

  // Protect against self.cur wraparound.
  if self.cur >= buffer_reset {
    self.shift_offsets()
  }
}

// shift_offsets will shift down all match offset.
// This is only called in rare situations to prevent integer overflow.
//
// See https://golang.org/issue/18636 and https://github.com/golang/go/issues/34121.

///|
fn DeflateFast::shift_offsets(self : DeflateFast) -> Unit {
  if self.prev.length() == 0 {
    // We have no history; just clear the table.
    for i = 0; i < table_size; i = i + 1 {
      self.table[i] = { val: 0U, offset: 0 }
    }
    self.cur = max_match_offset + 1
    return
  }

  // Shift down everything in the table that isn't already too far away.
  for i = 0; i < table_size; i = i + 1 {
    let mut v = self.table[i].offset - self.cur + max_match_offset + 1
    if v < 0 {
      // We want to reset self.cur to max_match_offset + 1, so we need to shift
      // all table entries down by (self.cur - (max_match_offset + 1)).
      // Because we ignore matches > max_match_offset, we can cap
      // any negative offsets at 0.
      v = 0
    }
    self.table[i].offset = v
  }
  self.cur = max_match_offset + 1
}