/*
 * Copyright 2023 Signal Messenger, LLC
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package org.signal.core.util

import java.text.NumberFormat
import kotlin.math.min

inline val Long.bytes: ByteSize
  get() = ByteSize(this)

inline val Int.bytes: ByteSize
  get() = ByteSize(this.toLong())

inline val Long.kibiBytes: ByteSize
  get() = (this * 1024).bytes

inline val Int.kibiBytes: ByteSize
  get() = (this.toLong() * 1024L).bytes

inline val Long.mebiBytes: ByteSize
  get() = (this * 1024L).kibiBytes

inline val Int.mebiBytes: ByteSize
  get() = (this.toLong() * 1024L).kibiBytes

inline val Long.gibiBytes: ByteSize
  get() = (this * 1024L).mebiBytes

inline val Int.gibiBytes: ByteSize
  get() = (this.toLong() * 1024L).mebiBytes

inline val Long.tebiBytes: ByteSize
  get() = (this * 1024L).gibiBytes

inline val Int.tebiBytes: ByteSize
  get() = (this.toLong() * 1024L).gibiBytes

class ByteSize(val bytes: Long) {
  val inWholeBytes: Long
    get() = bytes

  val inWholeKibiBytes: Long
    get() = bytes / 1024L

  val inWholeMebiBytes: Long
    get() = inWholeKibiBytes / 1024L

  val inWholeGibiBytes: Long
    get() = inWholeMebiBytes / 1024L

  val inWholeTebiBytes: Long
    get() = inWholeGibiBytes / 1024L

  val inKibiBytes: Float
    get() = bytes / 1024f

  val inMebiBytes: Float
    get() = inKibiBytes / 1024f

  val inGibiBytes: Float
    get() = inMebiBytes / 1024f

  val inTebiBytes: Float
    get() = inGibiBytes / 1024f

  fun getLargestNonZeroSize(): Size {
    return when {
      inWholeTebiBytes > 0L -> Size.TEBIBYTE
      inWholeGibiBytes > 0L -> Size.GIBIBYTE
      inWholeMebiBytes > 0L -> Size.MEBIBYTE
      inWholeKibiBytes > 0L -> Size.KIBIBYTE
      else -> Size.BYTE
    }
  }

  /** The value of this size expressed in [size] (e.g. [Size.MEBIBYTE] -> a count of mebibytes). */
  fun inUnit(size: Size): Float {
    return when (size) {
      Size.BYTE -> inWholeBytes.toFloat()
      Size.KIBIBYTE -> inKibiBytes
      Size.MEBIBYTE -> inMebiBytes
      Size.GIBIBYTE -> inGibiBytes
      Size.TEBIBYTE -> inTebiBytes
    }
  }

  @JvmOverloads
  fun toUnitString(maxPlaces: Int = 2, spaced: Boolean = true): String {
    return toUnitString(getLargestNonZeroSize(), maxPlaces, spaced)
  }

  /**
   * Format as a specific unit.
   *
   * @param unit The unit to use when rendering
   * @param maxPlaces Max number of digits to the right of the decimal
   * @param padDecimals If true add zeros as necessary to match [maxPlaces]
   * @param withUnit If true include the unit label in the text
   */
  @JvmOverloads
  fun toUnitString(unit: Size, maxPlaces: Int = 2, spaced: Boolean = true, padDecimals: Boolean = false, withUnit: Boolean = true): String {
    val size: Float = inUnit(unit)

    val places = when (unit) {
      Size.BYTE,
      Size.KIBIBYTE -> 0

      Size.MEBIBYTE -> min(1, maxPlaces)

      Size.GIBIBYTE,
      Size.TEBIBYTE -> min(2, maxPlaces)
    }

    val formatter = NumberFormat.getInstance().apply {
      minimumFractionDigits = if (padDecimals) places else 0
      maximumFractionDigits = places
    }

    val suffix = if (withUnit) "${if (spaced) " " else ""}${unit.label}" else ""
    return BidiUtil.forceLtr("${formatter.format(size)}$suffix")
  }

  operator fun compareTo(other: ByteSize): Int {
    return bytes.compareTo(other.bytes)
  }

  operator fun plus(other: ByteSize): ByteSize {
    return ByteSize(this.inWholeBytes + other.inWholeBytes)
  }

  fun percentageOf(other: ByteSize): Float {
    return this.inWholeBytes.toFloat() / other.inWholeBytes.toFloat()
  }

  operator fun minus(other: ByteSize): ByteSize {
    return ByteSize(this.inWholeBytes - other.inWholeBytes)
  }

  operator fun times(other: Long): ByteSize {
    return ByteSize(this.inWholeBytes * other)
  }

  override fun toString(): String {
    return "ByteSize(${toUnitString(maxPlaces = 4, spaced = false)})"
  }

  override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    other as ByteSize

    return bytes == other.bytes
  }

  override fun hashCode(): Int {
    return bytes.hashCode()
  }

  enum class Size(val label: String) {
    BYTE("B"),
    KIBIBYTE("KB"),
    MEBIBYTE("MB"),
    GIBIBYTE("GB"),
    TEBIBYTE("TB")
  }
}
