/*
 *  Copyright 2010-2013, Carrot Search s.c., Boznicza 11/56, Poznan, Poland
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package shark.internal.hppc

import java.util.Locale

/**
 * Code from com.carrotsearch.hppc.LongLongScatterMap copy pasted, inlined and converted to Kotlin.
 *
 * See https://github.com/carrotsearch/hppc .
 */
internal class LongLongScatterMap constructor(expectedElements: Int = 4) {

  fun interface ForEachCallback {
    fun onEntry(key: Long, value: Long)
  }

  /**
   * The array holding keys.
   */
  private var keys: LongArray = longArrayOf()

  /**
   * The array holding values.
   */
  private var values: LongArray = longArrayOf()

  /**
   * The number of stored keys (assigned key slots), excluding the special
   * "empty" key, if any (use [.size] instead).
   *
   * @see .size
   */
  private var assigned: Int = 0

  /**
   * Mask for slot scans in [.keys].
   */
  private var mask: Int = 0

  /**
   * Expand (rehash) [.keys] when [.assigned] hits this value.
   */
  private var resizeAt: Int = 0

  /**
   * Special treatment for the "empty slot" key marker.
   */
  private var hasEmptyKey: Boolean = false

  /**
   * The load factor for [.keys].
   */
  private var loadFactor: Double = 0.75

  val isEmpty: Boolean
    get() = size == 0

  init {
    ensureCapacity(expectedElements)
  }

  operator fun set(
    key: Long,
    value: Long
  ): Long {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = true
      val previousValue = values[mask + 1]
      values[mask + 1] = value
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          values[slot] = value
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      if (assigned == resizeAt) {
        allocateThenInsertThenRehash(slot, key, value)
      } else {
        keys[slot] = key
        values[slot] = value
      }

      assigned++
      return 0L
    }
  }

  fun remove(key: Long): Long {
    val mask = this.mask
    if (key == 0L) {
      hasEmptyKey = false
      val previousValue = values[mask + 1]
      values[mask + 1] = 0L
      return previousValue
    } else {
      val keys = this.keys
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          val previousValue = values[slot]
          shiftConflictingKeys(slot)
          return previousValue
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return 0L
    }
  }

  /**
   * Being given a key looks it up in the map and returns the slot where element sits, so it later
   * can be retrieved with [getSlotValue]; return '-1' if element not found.
   * Why so complicated and not just make [get] return null if value not found? The reason is performance:
   * this approach prevents unnecessary boxing of the primitive long that would happen with nullable Long?
   */
  fun getSlot(key: Long): Int {
    if (key == 0L) {
      return if (hasEmptyKey) mask + 1 else -1
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return slot
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return -1
    }
  }

  /**
   * Being given a slot of element retrieves it from the collection
   */
  fun getSlotValue(slot: Int): Long = values[slot]

  /**
   * Returns an element matching a provided [key]; throws [IllegalArgumentException] if element not found
   */
  operator fun get(key: Long): Long {
    val slot = getSlot(key)
    require(slot != -1) { "Unknown key $key" }

    return getSlotValue(slot)
  }

  fun forEach(forEachCallback: ForEachCallback) {
    val max = mask + 1
    var slot = -1

    exitWhile@ while (true) {
      if (slot < max) {
        var existing: Long
        slot++
        while (slot < max) {
          existing = keys[slot]
          if (existing != 0L) {
            forEachCallback.onEntry(existing, values[slot])
            continue@exitWhile
          }
          slot++
        }
      }

      if (slot == max && hasEmptyKey) {
        slot++
        forEachCallback.onEntry(0L, values[max])
        continue@exitWhile
      }
      break@exitWhile
    }
  }

  fun entrySequence(): Sequence<LongLongPair> {
    val max = mask + 1
    var slot = -1
    return generateSequence {
      if (slot < max) {
        var existing: Long
        slot++
        while (slot < max) {
          existing = keys[slot]
          if (existing != 0L) {
            return@generateSequence existing to values[slot]
          }
          slot++
        }
      }
      if (slot == max && hasEmptyKey) {
        slot++
        return@generateSequence 0L to values[max]
      }
      return@generateSequence null
    }
  }

  fun containsKey(key: Long): Boolean {
    if (key == 0L) {
      return hasEmptyKey
    } else {
      val keys = this.keys
      val mask = this.mask
      var slot = hashKey(key) and mask

      var existing = keys[slot]
      while (existing != 0L) {
        if (existing == key) {
          return true
        }
        slot = slot + 1 and mask
        existing = keys[slot]
      }

      return false
    }
  }

  fun release() {
    assigned = 0
    hasEmptyKey = false

    allocateBuffers(HPPC.minBufferSize(4, loadFactor))
  }

  val size: Int
    get() {
      return assigned + if (hasEmptyKey) 1 else 0
    }

  fun ensureCapacity(expectedElements: Int) {
    if (expectedElements > resizeAt) {
      val prevKeys = this.keys
      val prevValues = this.values
      allocateBuffers(HPPC.minBufferSize(expectedElements, loadFactor))
      if (!isEmpty) {
        rehash(prevKeys, prevValues)
      }
    }
  }

  private fun hashKey(key: Long): Int {
    return HPPC.mixPhi(key)
  }

  /**
   * Rehash from old buffers to new buffers.
   */
  private fun rehash(
    fromKeys: LongArray,
    fromValues: LongArray
  ) {
    // Rehash all stored key/value pairs into the new buffers.
    val keys = this.keys
    val values = this.values
    val mask = this.mask
    var existing: Long

    // Copy the zero element's slot, then rehash everything else.
    var from = fromKeys.size - 1
    keys[keys.size - 1] = fromKeys[from]
    values[values.size - 1] = fromValues[from]
    while (--from >= 0) {
      existing = fromKeys[from]
      if (existing != 0L) {
        var slot = hashKey(existing) and mask
        while (keys[slot] != 0L) {
          slot = slot + 1 and mask
        }
        keys[slot] = existing
        values[slot] = fromValues[from]
      }
    }
  }

  /**
   * Allocate new internal buffers. This method attempts to allocate
   * and assign internal buffers atomically (either allocations succeed or not).
   */
  private fun allocateBuffers(arraySize: Int) {

    // Ensure no change is done if we hit an OOM.
    val prevKeys = this.keys
    val prevValues = this.values
    try {
      val emptyElementSlot = 1
      this.keys = LongArray(arraySize + emptyElementSlot)
      this.values = LongArray(arraySize + emptyElementSlot)
    } catch (e: OutOfMemoryError) {
      this.keys = prevKeys
      this.values = prevValues
      throw RuntimeException(
        String.format(
          Locale.ROOT,
          "Not enough memory to allocate buffers for rehashing: %d -> %d",
          this.mask + 1,
          arraySize
        ), e
      )
    }

    this.resizeAt = HPPC.expandAtCount(arraySize, loadFactor)
    this.mask = arraySize - 1
  }

  /**
   * This method is invoked when there is a new key/ value pair to be inserted into
   * the buffers but there is not enough empty slots to do so.
   *
   * New buffers are allocated. If this succeeds, we know we can proceed
   * with rehashing so we assign the pending element to the previous buffer
   * (possibly violating the invariant of having at least one empty slot)
   * and rehash all keys, substituting new buffers at the end.
   */
  private fun allocateThenInsertThenRehash(
    slot: Int,
    pendingKey: Long,
    pendingValue: Long
  ) {

    // Try to allocate new buffers first. If we OOM, we leave in a consistent state.
    val prevKeys = this.keys
    val prevValues = this.values
    allocateBuffers(HPPC.nextBufferSize(mask + 1, size, loadFactor))

    // We have succeeded at allocating new data so insert the pending key/value at
    // the free slot in the old arrays before rehashing.
    prevKeys[slot] = pendingKey
    prevValues[slot] = pendingValue

    // Rehash old keys, including the pending key.
    rehash(prevKeys, prevValues)
  }

  /**
   * Shift all the slot-conflicting keys and values allocated to
   * (and including) `slot`.
   */
  private fun shiftConflictingKeys(gapSlotArg: Int) {
    var gapSlot = gapSlotArg
    val keys = this.keys
    val values = this.values
    val mask = this.mask

    // Perform shifts of conflicting keys to fill in the gap.
    var distance = 0
    while (true) {
      val slot = gapSlot + ++distance and mask
      val existing = keys[slot]
      if (existing == 0L) {
        break
      }

      val idealSlot = hashKey(existing)
      val shift = slot - idealSlot and mask
      if (shift >= distance) {
        // Entry at this position was originally at or before the gap slot.
        // Move the conflict-shifted entry to the gap's position and repeat the procedure
        // for any entries to the right of the current position, treating it
        // as the new gap.
        keys[gapSlot] = existing
        values[gapSlot] = values[slot]
        gapSlot = slot
        distance = 0
      }
    }

    // Mark the last found gap slot without a conflict as empty.
    keys[gapSlot] = 0L
    values[gapSlot] = 0L
    assigned--
  }
}
