// Copyright (c) 2017 PSForever
package net.psforever.objects

import net.psforever.objects.definition.{AmmoBoxDefinition, ProjectileDefinition, ToolDefinition}
import net.psforever.objects.equipment._
import net.psforever.objects.ballistics.Projectiles

import scala.annotation.tailrec

/**
  * A type of `Equipment` that can be wielded and loaded with certain other game elements.<br>
  * <br>
  * "Tool" is a very mechanical name while this class is intended for various weapons and support items.
  * The primary trait of a `Tool` is that it has something that counts as an "ammunition,"
  * depleted as the `Tool` is used, replaceable as long as one has an appropriate type of `AmmoBox` object.
  * (The former is always called "consuming;" the latter, "reloading.")
  * Some weapons Chainblade have ammunition but do not consume it.
  * @param toolDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields
  */
class Tool(private val toolDef: ToolDefinition)
    extends Equipment
    with FireModeSwitch[FireModeDefinition]
    with JammableUnit {
  private var tdef = toolDef
  /** index of the current fire mode on the `ToolDefinition`'s list of fire modes */
  private var fireModeIndex: Int = tdef.DefaultFireModeIndex
  /** current ammunition slot being used by this fire mode */
  private var ammoSlots: List[Tool.FireModeSlot] = List.empty
  var lastDischarge: Long                        = 0

  Tool.LoadDefinition(tool = this)

  def FireModeIndex: Int = fireModeIndex

  def FireModeIndex_=(index: Int): Int = {
    fireModeIndex = index % Definition.FireModes.length
    FireModeIndex
  }

  def FireMode: FireModeDefinition = Definition.FireModes(fireModeIndex)

  def NextFireMode: FireModeDefinition = {
    FireModeIndex = Definition.NextFireModeIndex(FireModeIndex)
    AmmoSlot.Chamber = FireMode.Chamber
    FireMode
  }

  def ToFireMode: Int = Definition.NextFireModeIndex(FireModeIndex)

  def ToFireMode_=(index: Int): FireModeDefinition = {
    FireModeIndex = index
    AmmoSlot.Chamber = FireMode.Chamber
    FireMode
  }

  def AmmoTypeIndex: Int = FireMode.AmmoTypeIndices(AmmoSlot.AmmoTypeIndex)

  def AmmoTypeIndex_=(index: Int): Int = {
    AmmoSlot.AmmoTypeIndex = index % FireMode.AmmoTypeIndices.length
    AmmoTypeIndex
  }

  def AmmoType: Ammo.Value = Definition.AmmoTypes(AmmoTypeIndex).AmmoType

  def NextAmmoType: Ammo.Value = {
    AmmoSlot.AmmoTypeIndex = AmmoSlot.AmmoTypeIndex + 1
    AmmoType
  }

  def Projectile: ProjectileDefinition = {
    Definition.ProjectileTypes({
      val projIndices = FireMode.ProjectileTypeIndices
      if (projIndices.isEmpty) {
        AmmoTypeIndex //e.g., bullet_9mm -> bullet_9mm_projectile, bullet_9mm_AP -> bullet_9mm_AP_projectile
      } else {
        projIndices(AmmoSlot.AmmoTypeIndex) //e.g., pulsar: f.mode1 -> pulsar_projectile, f.mode2 = pulsar_ap_projectile
      }
    })
  }

  def ProjectileType: Projectiles.Types.Value = Projectile.ProjectileType

  def Magazine: Int = AmmoSlot.Magazine

  def Magazine_=(mag: Int): Int = {
    //AmmoSlot.Magazine = Math.min(Math.max(0, mag), MaxMagazine)
    AmmoSlot.Magazine = Math.max(0, mag)
    Magazine
  }

  def MaxMagazine: Int = {
    val fmode = FireMode
    fmode.CustomMagazine.get(AmmoType) match {
      case Some(magSize) =>
        magSize
      case None =>
        fmode.Magazine
    }
  }

  def Discharge(rounds: Option[Int] = None): Int = {
    lastDischarge = System.nanoTime()
    Magazine = FireMode.Discharge(this, rounds)
  }

  def LastDischarge: Long = {
    lastDischarge
  }

  def AmmoSlot: Tool.FireModeSlot = ammoSlots(FireMode.AmmoSlotIndex)

  def AmmoSlots: List[Tool.FireModeSlot] = ammoSlots

  def MaxAmmoSlot: Int = ammoSlots.length

  def Definition: ToolDefinition = tdef

  override def toString: String = Tool.toString(this)
}

//AmmoType = Definition.AmmoTypes( (Definition.FireModes(fireModeIndex)).AmmoTypeIndices( (ammoSlots((Definition.FireModes(fireModeIndex)).AmmoSlotIndex)).AmmoTypeIndex) ).AmmoType

object Tool {
  def apply(toolDef: ToolDefinition): Tool = {
    new Tool(toolDef)
  }

  /**
    * Use the `*Definition` that was provided to this object to initialize its fields and settings.
    * @param tool the `Tool` being initialized
    */
  def LoadDefinition(tool: Tool): Unit = {
    val tdef       = tool.Definition
    val maxSlot    = tdef.FireModes.maxBy(fmode => fmode.AmmoSlotIndex).AmmoSlotIndex
    tool.ammoSlots = buildFireModes(tdef, (0 to maxSlot).iterator, tdef.FireModes.toList)
    tool.fireModeIndex = tdef.DefaultFireModeIndex
  }
  /**
    * Substitute this `Definition` for the one that was originally provided for this entity.
    * Calling this will not reconstruct the internal fields of the entity.
    * @param tool the `Tool` being modified
    * @param tdef the definition used to override the definition that was previously assigned this `Tool`;
    *             WILL override the assignment in the original constructor
    */
  def LoadDefinition(tool: Tool, tdef: ToolDefinition): Unit = {
    tool.tdef = tdef
  }

  @tailrec private def buildFireModes(
      tdef: ToolDefinition,
      iter: Iterator[Int],
      fmodes: List[FireModeDefinition],
      list: List[FireModeSlot] = Nil
  ): List[FireModeSlot] = {
    if (!iter.hasNext) {
      list
    } else {
      val index = iter.next()
      fmodes.filter(fmode => fmode.AmmoSlotIndex == index) match {
        case fmode :: _ =>
          buildFireModes(tdef, iter, fmodes, list :+ new FireModeSlot(tdef, fmode))
        case Nil =>
          throw new IllegalArgumentException(
            s"tool ${tdef.Name} ammo slot #$index is missing a fire mode specification; do not skip"
          )
      }
    }
  }

  def toString(obj: Tool): String = {
    s"${obj.Definition.Name} (mode=${obj.FireModeIndex}-${obj.AmmoType})(${obj.Magazine}/${obj.MaxMagazine})"
  }

  /**
    * The `FireModeSlot` can be called the "magazine feed," an abstracted "ammunition slot."
    * Most weapons will have only one ammunition slot and swap different ammunition into it as needed.
    * In general to swap ammunition means to unload the onld ammunition and load the new ammunition.
    * Many weapons also have one ammunition slot and multiple fire modes using the same list of ammunition
    * This slot manages either of two ammunitions where one does not need to unload to be swapped to the other;
    * however, the fire mod has most likely been changed.
    * The Punisher -
    * six ammunition types in total,
    * two uniquely different types without unloading,
    * two exclusive groups of ammunition divided into 2 cycled types and 4 cycled types -
    * is an example of a weapon that benefits from this implementation.
    */
  class FireModeSlot(private val tdef: ToolDefinition, private val fdef: FireModeDefinition) {

    /**
      * if this fire mode has multiple types of ammunition
      * this is the index of the fire mode's ammo List, not a reference to the tool's ammo List
      */
    private var ammoTypeIndex: Int = 0

    /** a reference to the actual `AmmoBox` of this slot */
    private var box: AmmoBox = AmmoBox(AmmoDefinition, MaxMagazine())
    private var chamber      = fdef.Chamber

    def AmmoTypeIndex: Int = ammoTypeIndex

    def AmmoTypeIndex_=(index: Int): Int = {
      ammoTypeIndex = index % fdef.AmmoTypeIndices.length
      AmmoTypeIndex
    }

    private def AmmoDefinition: AmmoBoxDefinition = {
      tdef.AmmoTypes(fdef.AmmoTypeIndices(ammoTypeIndex))
    }

    /**
      * This is a reference to the `Ammo.Value` whose `AmmoBoxDefinition` should be loaded into `box`.
      * It may not be the correct `Ammo.Value` whose `AmmoBoxDefinition` is loaded into `box` such as is the case during ammunition swaps.
      * Generally, convert from this index, to the index in the fire mode's ammunition list, to the index in the `ToolDefinition`'s ammunition list.
      * @return the `Ammo` type that should be loaded into the magazine right now
      */
    def AmmoType: Ammo.Value = AmmoDefinition.AmmoType

    def AllAmmoTypes: List[Ammo.Value] = {
      fdef.AmmoTypeIndices.map(index => tdef.AmmoTypes(fdef.AmmoTypeIndices(index)).AmmoType).toList
    }

    def Magazine: Int = box.Capacity

    def Magazine_=(mag: Int): Int = {
      box.Capacity = mag
      Magazine
    }

    def Chamber: Int = chamber

    def Chamber_=(chmbr: Int): Int = {
      chamber = math.min(math.max(0, chmbr), fdef.Chamber)
      Chamber
    }

    def MaxMagazine(): Int = {
      fdef.CustomMagazine.get(AmmoType) match {
        case Some(value) => value
        case None        => fdef.DefaultMagazine
      }
    }

    def Box: AmmoBox = box

    def Box_=(toBox: AmmoBox): Option[AmmoBox] = {
      if (toBox.AmmoType == AmmoType) {
        box = toBox
        Some(Box)
      } else {
        None
      }
    }

    def Tool: ToolDefinition = tdef

    def Definition: FireModeDefinition = fdef
  }
}
