/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2009-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package org.apache.pekko.cluster.ddata

import java.util.concurrent.atomic.AtomicLong

import scala.annotation.tailrec
import scala.collection.immutable.TreeMap

import org.apache.pekko
import pekko.annotation.InternalApi
import pekko.cluster.Cluster
import pekko.cluster.UniqueAddress

/**
 * VersionVector module with helper classes and methods.
 */
object VersionVector {

  private val emptyVersions: TreeMap[UniqueAddress, Long] = TreeMap.empty
  val empty: VersionVector = ManyVersionVector(emptyVersions)

  def apply(): VersionVector = empty

  def apply(versions: TreeMap[UniqueAddress, Long]): VersionVector =
    if (versions.isEmpty) empty
    else if (versions.size == 1) apply(versions.head._1, versions.head._2)
    else ManyVersionVector(versions)

  def apply(node: UniqueAddress, version: Long): VersionVector = OneVersionVector(node, version)

  /** INTERNAL API */
  @InternalApi private[pekko] def apply(versions: List[(UniqueAddress, Long)]): VersionVector =
    if (versions.isEmpty) empty
    else if (versions.tail.isEmpty) apply(versions.head._1, versions.head._2)
    else apply(emptyVersions ++ versions)

  /**
   * Java API
   */
  def create(): VersionVector = empty

  sealed trait Ordering
  case object After extends Ordering
  case object Before extends Ordering
  case object Same extends Ordering
  case object Concurrent extends Ordering

  /**
   * Marker to ensure that we do a full order comparison instead of bailing out early.
   */
  private case object FullOrder extends Ordering

  /**
   * Java API: The `VersionVector.After` instance
   */
  def AfterInstance = After

  /**
   * Java API: The `VersionVector.Before` instance
   */
  def BeforeInstance = Before

  /**
   * Java API: The `VersionVector.Same` instance
   */
  def SameInstance = Same

  /**
   * Java API: The `VersionVector.Concurrent` instance
   */
  def ConcurrentInstance = Concurrent

  /** INTERNAL API */
  @InternalApi private[pekko] object Timestamp {
    final val Zero = 0L
    final val EndMarker = Long.MinValue
    val counter = new AtomicLong(1L)
  }

  /**
   * Marker to signal that we have reached the end of a version vector.
   */
  private val cmpEndMarker = (null, Timestamp.EndMarker)

}

/**
 * Representation of a Vector-based clock (counting clock), inspired by Lamport logical clocks.
 * {{{
 * Reference:
 *    1) Leslie Lamport (1978). "Time, clocks, and the ordering of events in a distributed system". Communications of the ACM 21 (7): 558-565.
 *    2) Friedemann Mattern (1988). "Virtual Time and Global States of Distributed Systems". Workshop on Parallel and Distributed Algorithms: pp. 215-226
 * }}}
 *
 * Based on code from `org.apache.pekko.cluster.VectorClock`.
 *
 * This class is immutable, i.e. "modifying" methods return a new instance.
 */
@SerialVersionUID(1L)
sealed abstract class VersionVector extends ReplicatedData with ReplicatedDataSerialization with RemovedNodePruning {

  type T = VersionVector

  import VersionVector._

  /**
   * Increment the version for the node passed as argument. Returns a new VersionVector.
   */
  def :+(node: SelfUniqueAddress): VersionVector = increment(node)

  @deprecated("Use `:+` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
  def +(node: Cluster): VersionVector = increment(node.selfUniqueAddress)

  /**
   * INTERNAL API
   * Increment the version for the node passed as argument. Returns a new VersionVector.
   */
  @InternalApi private[pekko] def +(node: UniqueAddress): VersionVector = increment(node)

  /**
   * Increment the version for the node passed as argument. Returns a new VersionVector.
   */
  def increment(node: SelfUniqueAddress): VersionVector = increment(node.uniqueAddress)

  @deprecated("Use `increment` that takes a `SelfUniqueAddress` parameter instead.", since = "Akka 2.5.20")
  def increment(node: Cluster): VersionVector = increment(node.selfUniqueAddress)

  def isEmpty: Boolean

  /**
   * INTERNAL API
   */
  @InternalApi private[pekko] def size: Int

  /**
   * INTERNAL API
   * Increment the version for the node passed as argument. Returns a new VersionVector.
   */
  @InternalApi private[pekko] def increment(node: UniqueAddress): VersionVector

  /**
   * INTERNAL API
   */
  @InternalApi private[pekko] def versionAt(node: UniqueAddress): Long

  /**
   * INTERNAL API
   */
  @InternalApi private[pekko] def contains(node: UniqueAddress): Boolean

  /**
   * Returns true if <code>this</code> and <code>that</code> are concurrent else false.
   */
  def <>(that: VersionVector): Boolean = compareOnlyTo(that, Concurrent) eq Concurrent

  /**
   * Returns true if <code>this</code> is before <code>that</code> else false.
   */
  def <(that: VersionVector): Boolean = compareOnlyTo(that, Before) eq Before

  /**
   * Returns true if <code>this</code> is after <code>that</code> else false.
   */
  def >(that: VersionVector): Boolean = compareOnlyTo(that, After) eq After

  /**
   * Returns true if this VersionVector has the same history as the 'that' VersionVector else false.
   */
  def ==(that: VersionVector): Boolean = compareOnlyTo(that, Same) eq Same

  /**
   * Version vector comparison according to the semantics described by compareTo, with the ability to bail
   * out early if the we can't reach the Ordering that we are looking for.
   *
   * The ordering always starts with Same and can then go to Same, Before or After
   * If we're on After we can only go to After or Concurrent
   * If we're on Before we can only go to Before or Concurrent
   * If we go to Concurrent we exit the loop immediately
   *
   * If you send in the ordering FullOrder, you will get a full comparison.
   */
  private final def compareOnlyTo(that: VersionVector, order: Ordering): Ordering = {
    def nextOrElse[A](iter: Iterator[A], default: A): A = if (iter.hasNext) iter.next() else default

    def compare(
        i1: Iterator[(UniqueAddress, Long)],
        i2: Iterator[(UniqueAddress, Long)],
        requestedOrder: Ordering): Ordering = {
      @tailrec
      def compareNext(nt1: (UniqueAddress, Long), nt2: (UniqueAddress, Long), currentOrder: Ordering): Ordering =
        if ((requestedOrder ne FullOrder) && (currentOrder ne Same) && (currentOrder ne requestedOrder)) currentOrder
        else if ((nt1 eq cmpEndMarker) && (nt2 eq cmpEndMarker)) currentOrder
        // i1 is empty but i2 is not, so i1 can only be Before
        else if (nt1 eq cmpEndMarker) {
          if (currentOrder eq After) Concurrent else Before
        }
        // i2 is empty but i1 is not, so i1 can only be After
        else if (nt2 eq cmpEndMarker) {
          if (currentOrder eq Before) Concurrent else After
        } else {
          // compare the nodes
          val nc = nt1._1.compareTo(nt2._1)
          if (nc == 0) {
            // both nodes exist compare the timestamps
            // same timestamp so just continue with the next nodes
            if (nt1._2 == nt2._2) compareNext(nextOrElse(i1, cmpEndMarker), nextOrElse(i2, cmpEndMarker), currentOrder)
            else if (nt1._2 < nt2._2) {
              // t1 is less than t2, so i1 can only be Before
              if (currentOrder eq After) Concurrent
              else compareNext(nextOrElse(i1, cmpEndMarker), nextOrElse(i2, cmpEndMarker), Before)
            } else {
              // t2 is less than t1, so i1 can only be After
              if (currentOrder eq Before) Concurrent
              else compareNext(nextOrElse(i1, cmpEndMarker), nextOrElse(i2, cmpEndMarker), After)
            }
          } else if (nc < 0) {
            // this node only exists in i1 so i1 can only be After
            if (currentOrder eq Before) Concurrent
            else compareNext(nextOrElse(i1, cmpEndMarker), nt2, After)
          } else {
            // this node only exists in i2 so i1 can only be Before
            if (currentOrder eq After) Concurrent
            else compareNext(nt1, nextOrElse(i2, cmpEndMarker), Before)
          }
        }

      compareNext(nextOrElse(i1, cmpEndMarker), nextOrElse(i2, cmpEndMarker), Same)
    }

    if (this eq that) Same
    else compare(this.versionsIterator, that.versionsIterator, if (order eq Concurrent) FullOrder else order)
  }

  /**
   * INTERNAL API
   */
  @InternalApi private[pekko] def versionsIterator: Iterator[(UniqueAddress, Long)]

  /**
   * Compare two version vectors. The outcome will be one of the following:
   * <p/>
   * {{{
   *   1. Version 1 is SAME (==)       as Version 2 iff for all i c1(i) == c2(i)
   *   2. Version 1 is BEFORE (<)      Version 2 iff for all i c1(i) <= c2(i) and there exist a j such that c1(j) < c2(j)
   *   3. Version 1 is AFTER (>)       Version 2 iff for all i c1(i) >= c2(i) and there exist a j such that c1(j) > c2(j).
   *   4. Version 1 is CONCURRENT (<>) to Version 2 otherwise.
   * }}}
   */
  def compareTo(that: VersionVector): Ordering = {
    compareOnlyTo(that, FullOrder)
  }

  /**
   * Merges this VersionVector with another VersionVector. E.g. merges its versioned history.
   */
  def merge(that: VersionVector): VersionVector

  override def needPruningFrom(removedNode: UniqueAddress): Boolean

  override def prune(removedNode: UniqueAddress, collapseInto: UniqueAddress): VersionVector

  override def pruningCleanup(removedNode: UniqueAddress): VersionVector

  /** INTERNAL API */
  @InternalApi private[pekko] def estimatedSize: Int

}

final case class OneVersionVector private[pekko] (node: UniqueAddress, version: Long) extends VersionVector {
  import VersionVector.Timestamp

  override def isEmpty: Boolean = false

  /** INTERNAL API */
  @InternalApi private[pekko] override def size: Int = 1

  /** INTERNAL API */
  @InternalApi private[pekko] override def increment(n: UniqueAddress): VersionVector = {
    val v = Timestamp.counter.getAndIncrement()
    if (n == node) copy(version = v)
    else ManyVersionVector(TreeMap(node -> version, n -> v))
  }

  /** INTERNAL API */
  @InternalApi private[pekko] override def versionAt(n: UniqueAddress): Long =
    if (n == node) version
    else Timestamp.Zero

  /** INTERNAL API */
  @InternalApi private[pekko] override def contains(n: UniqueAddress): Boolean =
    n == node

  /** INTERNAL API */
  @InternalApi private[pekko] override def versionsIterator: Iterator[(UniqueAddress, Long)] =
    Iterator.single((node, version))

  override def merge(that: VersionVector): VersionVector = {
    that match {
      case OneVersionVector(n2, v2) =>
        if (node == n2) if (version >= v2) this else OneVersionVector(n2, v2)
        else ManyVersionVector(TreeMap(node -> version, n2 -> v2))
      case ManyVersionVector(vs2) =>
        val v2 = vs2.getOrElse(node, Timestamp.Zero)
        val mergedVersions =
          if (v2 >= version) vs2
          else vs2.updated(node, version)
        VersionVector(mergedVersions)
    }
  }

  override def modifiedByNodes: Set[UniqueAddress] =
    Set(node)

  override def needPruningFrom(removedNode: UniqueAddress): Boolean =
    node == removedNode

  override def prune(removedNode: UniqueAddress, collapseInto: UniqueAddress): VersionVector =
    (if (node == removedNode) VersionVector.empty else this) + collapseInto

  override def pruningCleanup(removedNode: UniqueAddress): VersionVector =
    if (node == removedNode) VersionVector.empty else this

  override def toString: String =
    s"VersionVector($node -> $version)"

  /** INTERNAL API */
  @InternalApi override private[pekko] def estimatedSize: Int =
    EstimatedSize.UniqueAddress + EstimatedSize.LongValue

}

final case class ManyVersionVector(versions: TreeMap[UniqueAddress, Long]) extends VersionVector {
  import VersionVector.Timestamp

  override def isEmpty: Boolean = versions.isEmpty

  /** INTERNAL API */
  @InternalApi private[pekko] override def size: Int = versions.size

  /** INTERNAL API */
  @InternalApi private[pekko] override def increment(node: UniqueAddress): VersionVector = {
    val v = Timestamp.counter.getAndIncrement()
    VersionVector(versions.updated(node, v))
  }

  /** INTERNAL API */
  @InternalApi private[pekko] override def versionAt(node: UniqueAddress): Long = versions.get(node) match {
    case Some(v) => v
    case None    => Timestamp.Zero
  }

  /** INTERNAL API */
  @InternalApi private[pekko] override def contains(node: UniqueAddress): Boolean =
    versions.contains(node)

  /** INTERNAL API */
  @InternalApi private[pekko] override def versionsIterator: Iterator[(UniqueAddress, Long)] =
    versions.iterator

  override def merge(that: VersionVector): VersionVector = {
    if (that.isEmpty) this
    else if (this.isEmpty) that
    else
      that match {
        case ManyVersionVector(vs2) =>
          var mergedVersions = vs2
          for ((node, time) <- versions) {
            val mergedVersionsCurrentTime = mergedVersions.getOrElse(node, Timestamp.Zero)
            if (time > mergedVersionsCurrentTime)
              mergedVersions = mergedVersions.updated(node, time)
          }
          VersionVector(mergedVersions)
        case OneVersionVector(n2, v2) =>
          val v1 = versions.getOrElse(n2, Timestamp.Zero)
          val mergedVersions =
            if (v1 >= v2) versions
            else versions.updated(n2, v2)
          VersionVector(mergedVersions)
      }
  }

  override def modifiedByNodes: Set[UniqueAddress] =
    versions.keySet

  override def needPruningFrom(removedNode: UniqueAddress): Boolean =
    versions.contains(removedNode)

  override def prune(removedNode: UniqueAddress, collapseInto: UniqueAddress): VersionVector =
    VersionVector(versions = versions - removedNode) + collapseInto

  override def pruningCleanup(removedNode: UniqueAddress): VersionVector =
    if (versions.contains(removedNode)) VersionVector(versions = versions - removedNode)
    else this

  override def toString: String =
    versions.map { case ((n, v)) => n.toString + " -> " + v }.mkString("VersionVector(", ", ", ")")

  /** INTERNAL API */
  @InternalApi override private[pekko] def estimatedSize: Int =
    versions.size * (EstimatedSize.UniqueAddress + EstimatedSize.LongValue)
}
