/*
 * 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.bytes.MutableBytes
import net.consensys.cava.crypto.Hash.keccak256
import net.consensys.cava.rlp.RLP
import java.lang.ref.WeakReference

private val NULL_NODE: NullNode<*> = NullNode.instance<Any>()

internal class BranchNode<V>(
  private val children: List<Node<V>>,
  private val value: V?,
  private val nodeFactory: NodeFactory<V>,
  private val valueSerializer: (V) -> Bytes
) : Node<V> {

  companion object {
    const val RADIX = CompactEncoding.LEAF_TERMINATOR.toInt()
  }

  @Volatile
  private var rlp: WeakReference<Bytes>? = null
  @Volatile
  private var hash: Bytes32? = null

  init {
    assert(children.size == RADIX)
  }

  override suspend fun accept(visitor: NodeVisitor<V>, path: Bytes): Node<V> = visitor.visit(this, path)

  override suspend fun path(): Bytes = Bytes.EMPTY

  override suspend fun value(): V? = value

  fun child(index: Byte): Node<V> = children[index.toInt()]

  override fun rlp(): Bytes {
    val prevEncoded = rlp?.get()
    if (prevEncoded != null) {
      return prevEncoded
    }
    val encoded = RLP.encodeList { out ->
      for (i in 0 until RADIX) {
        out.writeRLP(children[i].rlpRef())
      }
      if (value != null) {
        out.writeValue(valueSerializer(value))
      } else {
        out.writeValue(Bytes.EMPTY)
      }
    }
    rlp = WeakReference(encoded)
    return encoded
  }

  override fun rlpRef(): Bytes {
    val rlp = rlp()
    return if (rlp.size() < 32) rlp else RLP.encodeValue(hash())
  }

  override fun hash(): Bytes32 {
    hash?.let { return it }
    val hashed = keccak256(rlp())
    hash = hashed
    return hashed
  }

  override suspend fun replacePath(path: Bytes): Node<V> = nodeFactory.createExtension(path, this)

  suspend fun replaceChild(index: Byte, updatedChild: Node<V>): Node<V> {
    val newChildren = ArrayList(children)
    newChildren[index.toInt()] = updatedChild

    if (updatedChild === NULL_NODE) {
      if (value != null && !hasChildren()) {
        return nodeFactory.createLeaf(Bytes.of(index), value)
      } else if (value == null) {
        val flattened = maybeFlatten(newChildren)
        if (flattened != null) {
          return flattened
        }
      }
    }

    return nodeFactory.createBranch(newChildren, value)
  }

  suspend fun replaceValue(value: V): Node<V> = nodeFactory.createBranch(children, value)

  suspend fun removeValue(): Node<V> = maybeFlatten(children) ?: nodeFactory.createBranch(children, null)

  private fun hasChildren(): Boolean {
    for (child in children) {
      if (child !== NULL_NODE) {
        return true
      }
    }
    return false
  }
}

private suspend fun <V> maybeFlatten(children: List<Node<V>>): Node<V>? {
  val onlyChildIndex = findOnlyChild(children)
  if (onlyChildIndex < 0) {
    return null
  }

  val onlyChild = children[onlyChildIndex]

  // replace the path of the only child and return it
  val onlyChildPath = onlyChild.path()
  val completePath = MutableBytes.create(1 + onlyChildPath.size())
  completePath.set(0, onlyChildIndex.toByte())
  onlyChildPath.copyTo(completePath, 1)
  return onlyChild.replacePath(completePath)
}

private fun <V> findOnlyChild(children: List<Node<V>>): Int {
  var onlyChildIndex = -1
  assert(children.size == BranchNode.RADIX.toInt())
  for (i in 0 until BranchNode.RADIX) {
    if (children[i] !== NULL_NODE) {
      if (onlyChildIndex >= 0) {
        return -1
      }
      onlyChildIndex = i
    }
  }
  return onlyChildIndex
}
