/*
 * Copyright 2018 ConsenSys AG.
 *
 * 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 net.consensys.cava.trie

import net.consensys.cava.bytes.Bytes
import net.consensys.cava.bytes.Bytes32
import net.consensys.cava.trie.CompactEncoding.bytesToPath
import net.consensys.cava.trie.MerkleTrie.Companion.EMPTY_TRIE_ROOT_HASH
import java.util.function.Function

/**
 * A [MerkleTrie] that persists trie nodes to a [MerkleStorage] key/value store.
 *
 * @param <V> The type of values stored by this trie.
 */
class StoredMerklePatriciaTrie<V> : MerkleTrie<Bytes, V> {

  companion object {
    /**
     * Create a trie with value of type [Bytes].
     *
     * @param storage The storage to use for persistence.
     */
    @JvmStatic
    fun storingBytes(storage: MerkleStorage): StoredMerklePatriciaTrie<Bytes> =
      StoredMerklePatriciaTrie(storage, ::bytesIdentity, ::bytesIdentity)

    /**
     * Create a trie with keys and values of type [Bytes].
     *
     * @param storage The storage to use for persistence.
     * @param rootHash The initial root has for the trie, which should be already present in `storage`.
     */
    @JvmStatic
    fun storingBytes(storage: MerkleStorage, rootHash: Bytes32): StoredMerklePatriciaTrie<Bytes> =
      StoredMerklePatriciaTrie(storage, rootHash, ::bytesIdentity, ::bytesIdentity)

    /**
     * Create a trie with value of type [String].
     *
     * Strings are stored in UTF-8 encoding.
     *
     * @param storage The storage to use for persistence.
     */
    @JvmStatic
    fun storingStrings(storage: MerkleStorage): StoredMerklePatriciaTrie<String> =
      StoredMerklePatriciaTrie(storage, ::stringSerializer, ::stringDeserializer)

    /**
     * Create a trie with keys and values of type [String].
     *
     * Strings are stored in UTF-8 encoding.
     *
     * @param storage The storage to use for persistence.
     * @param rootHash The initial root has for the trie, which should be already present in `storage`.
     */
    @JvmStatic
    fun storingStrings(storage: MerkleStorage, rootHash: Bytes32): StoredMerklePatriciaTrie<String> =
      StoredMerklePatriciaTrie(storage, rootHash, ::stringSerializer, ::stringDeserializer)

    /**
     * Create a trie.
     *
     * @param storage The storage to use for persistence.
     * @param valueSerializer A function for serializing values to bytes.
     * @param valueDeserializer A function for deserializing values from bytes.
     * @param <V> The serialized type.
     * @return A new merkle trie.
     */
    @JvmStatic
    fun <V> create(
      storage: MerkleStorage,
      valueSerializer: Function<V, Bytes>,
      valueDeserializer: Function<Bytes, V>
    ): StoredMerklePatriciaTrie<V> {
      return StoredMerklePatriciaTrie(storage, valueSerializer::apply, valueDeserializer::apply)
    }

    /**
     * Create a trie.
     *
     * @param storage The storage to use for persistence.
     * @param rootHash The initial root has for the trie, which should be already present in `storage`.
     * @param valueSerializer A function for serializing values to bytes.
     * @param valueDeserializer A function for deserializing values from bytes.
     * @param <V> The serialized type.
     * @return A new merkle trie.
     */
    @JvmStatic
    fun <V> create(
      storage: MerkleStorage,
      rootHash: Bytes32,
      valueSerializer: Function<V, Bytes>,
      valueDeserializer: Function<Bytes, V>
    ): StoredMerklePatriciaTrie<V> {
      return StoredMerklePatriciaTrie(storage, rootHash, valueSerializer::apply, valueDeserializer::apply)
    }
  }

  private val getVisitor = GetVisitor<V>()
  private val removeVisitor = RemoveVisitor<V>()
  private val storage: MerkleStorage
  private val nodeFactory: StoredNodeFactory<V>
  private var root: Node<V>

  /**
   * Create a trie.
   *
   * @param storage The storage to use for persistence.
   * @param valueSerializer A function for serializing values to bytes.
   * @param valueDeserializer A function for deserializing values from bytes.
   */
  constructor(
    storage: MerkleStorage,
    valueSerializer: (V) -> Bytes,
    valueDeserializer: (Bytes) -> V
  ) : this(storage, EMPTY_TRIE_ROOT_HASH, valueSerializer, valueDeserializer)

  /**
   * Create a trie.
   *
   * @param storage The storage to use for persistence.
   * @param rootHash The initial root has for the trie, which should be already present in `storage`.
   * @param valueSerializer A function for serializing values to bytes.
   * @param valueDeserializer A function for deserializing values from bytes.
   */
  constructor(
    storage: MerkleStorage,
    rootHash: Bytes32,
    valueSerializer: (V) -> Bytes,
    valueDeserializer: (Bytes) -> V
  ) {
    this.storage = storage
    this.nodeFactory = StoredNodeFactory(storage, valueSerializer, valueDeserializer)

    this.root = if (rootHash == EMPTY_TRIE_ROOT_HASH) {
      NullNode.instance()
    } else {
      StoredNode(nodeFactory, rootHash)
    }
  }

  override suspend fun get(key: Bytes): V? = root.accept(getVisitor, bytesToPath(key)).value()

  override suspend fun put(key: Bytes, value: V?) {
    if (value == null) {
      return remove(key)
    }
    updateRoot(root.accept(PutVisitor(nodeFactory, value), bytesToPath(key)))
  }

  override suspend fun remove(key: Bytes) = updateRoot(root.accept(removeVisitor, bytesToPath(key)))

  override fun rootHash(): Bytes32 = root.hash()

  /**
   * Forces any cached trie nodes to be released, so they can be garbage collected.
   *
   * Note: nodes are already stored using [java.lang.ref.SoftReference]'s, so they will be released automatically
   * based on memory demands.
   */
  fun clearCache() {
    val currentRoot = root
    if (currentRoot is StoredNode<*>) {
      currentRoot.unload()
    }
  }

  private suspend fun updateRoot(newRoot: Node<V>) {
    this.root = if (newRoot is StoredNode<*>) {
      newRoot
    } else {
      storage.put(newRoot.hash(), newRoot.rlp())
      StoredNode(nodeFactory, newRoot)
    }
  }

  /**
   * @return A string representation of the object.
   */
  override fun toString(): String {
    return javaClass.simpleName + "[" + rootHash() + "]"
  }
}
