package o1.world

import scala.math.*
import scala.annotation.targetName

/** Each instance of this class represents a direction on a two-dimensional plane. A direction
  * consists of two components: `dx` and `dy`, each of which is a number between -1.0 and +1.0
  * (inclusive). `dx` indicates the degree to which the x coordinate changes when moving in that
  * direction; `dy` is the same for the y coordinate. The x coordinate increases rightwards and
  * y downwards.
  *
  * For example, moving straight rightwards can be thought of a moving in a `Direction` with a
  * `dx` of +1.0 and a `dy` of 0.0; by comparison, the upward direction would have a `dx` of 0.0
  * and a `dy` of -1.0. . Diagonals have non-zero values for both `dx` and `dy`.
  *
  * The `dx` and `dy` of any `Direction` always sum up to (roughly) 1.0, with the exception of the
  * special value [[Direction.NoDirection NoDirection]] which represents the lack of a direction
  * and as zero for both components. (In this, a `Direction` is different than a [[Velocity]],
  * which is a combination of a direction of movement and a speed.)
  *
  * You don’t instantiate `Direction` directly; instead, you create `Direction`s with the methods
  * on this class’s [[Direction$ companion object]]. Among other things, you can:
  *
  *  - construct a direction from two differences between
  *    coordinates, as in `Direction.fromDeltas(-100, 50)`; or
  *  - create a direction that corresponds to a given angle, as
  *    in `Direction.fromDegrees(60)` and
  *    `Direction.fromRadians(-scala.math.Pi / 4)`.
  *
  * Some of the methods of a `Direction` object also create and return new `Direction`s,
  * as does the [[Pos.directionOf directionOf]] method on `Pos` objects. Moreover, this
  * class’s [[Direction$ companion object]] has a few predefined `Direction` constants:
  * [[Direction.Up Up]] [[Direction.Down Down]], [[Direction.Left Left]],
  * [[Direction.Right Right]], and [[Direction.NoDirection NoDirection]].
  *
  * `Direction` objects are immutable.
  *
  * This class has an alias in the top-level package [[o1]], so it’s accessible to students
  * simply via `import o1.*`.
  *
  * @param dx  the amount of change in the x coordinate when moving in this direction, relative to `dy`
  * @param dy  the amount of change in the y coordinate when moving in this direction, relative to `dx` */
sealed case class Direction private(val dx: Double, val dy: Double) derives CanEqual: // dx*dx+dy*dy ~= 1, or both zero

  /** Returns this direction as radians. (Zero radians points rightwards, positive angles are
    * counterclockwise from there, negative ones clockwise.) */
  def toRadians: Double =
    val atan = atan2(-this.dy, this.dx)
    if atan < 0 then atan + 2 * Pi else atan

  /** Returns this direction as degrees. (Zero degrees points rightwards, positive angles are
    * counterclockwise from there, negative ones clockwise.) */
  def toDegrees: Double = this.toRadians.toDegrees


  /** Returns a direction that has the opposite `dx` as this one and the same `dy`. */
  def switchX: Direction = Direction(-dx,  dy)
  /** Returns a direction that has the opposite `dy` as this one and the same `dx`. */
  def switchY: Direction = Direction( dx, -dy)

  /** Returns a direction that has the opposite `dx` and the opposite `dy` as this one. */
  def opposite: Direction = Direction(-dx, -dy)


  /** Returns `true` if this direction is a clearly rightward one (with a `dx` over zero) or at least
    * non-leftward (with a `dx` of exactly zero). Only returns `false` if `dx` is less than zero. */
  def isRightward: Boolean = this.dx >= 0
  /** Returns `true` if this direction is a clearly leftward one (with a `dx` under zero) or at least
    * non-rightward (with a `dx` of exactly zero). Only returns `false` if `dx` is over zero. */
  def isLeftward: Boolean = this.dx <= 0
  /** Returns `true` if this direction is a clearly downward one (with a `dy` over zero) or at least
    * non-upward (with a `dy` of exactly zero). Only returns `false` if `dy` is less than zero. */
  def isDownward: Boolean = this.dy >= 0
  /** Returns `true` if this direction is a clearly upward one (with a `dy` under zero) or at least
    * non-downward (with a `dy` of exactly zero). Only returns `false` if `dy` is over zero. */
  def isUpward: Boolean = this.dy <= 0


  /** Determines whether this direction and the given one point towards the same quarter of the unit circle.
    * That is, determines whether the two directions’ `dx` have same sign as the other and whether
    * their `dy`s also have the same signs. Zero values count as both positive and negative for this purpose. */
  def sharesQuadrant(another: Direction): Boolean =
    def sameHalf(dx1: Double, dx2: Double) = dx1.sign == dx2.sign || dx1.sign == 0 || dx2.sign == 0
    sameHalf(this.dx, another.dx) && sameHalf(this.dy, another.dy)


  /** Returns a direction that is removed from this direction by the given number of degrees counterclockwise.
    * For instance, `counterclockwise(90)` produces a direction at a right angle to the original. */
  def counterclockwise(degrees: Double): Direction = Direction.fromDegrees(this.toDegrees + degrees)
  /** Returns a direction that is removed from this direction by the given number of degrees clockwise.
    * For instance, `clockwise(90)` produces a direction at a right angle to the original. */
  def clockwise(degrees: Double): Direction = Direction.fromDegrees(this.toDegrees - degrees)


  /** Returns a direction that is, clockwise, removed from this direction by a number of degrees specified
    * by the given direction. Calling `dir1 + dir2` is equivalent to calling `dir1.counterclockwise(dir2.toDegrees)` */
  @targetName("plus")
  def +(another: Direction): Direction = Direction.fromDegrees(this.toDegrees + another.toDegrees)


  /** Returns a `String` description of the `Direction`. The exact format varies for different direction objects.
    * @see [[roughly]] */
  override def toString = s"Direction(dx=$dx,dy=$dy)"

  /** Returns a `String` description of the `Direction`; e.g., `"dx=1.00,dy=0.00"`.
    * Uses two decimals for each coordinate. */
  def roughly: String = f"dx=$dx%1.2f,dy=$dy%1.2f"

end Direction



/** This companion object of [[Direction class `Direction`]] provides some constants of type `Direction`
  * and methods for creating new `Direction` instances.
  *
  * The object has an alias in the top-level package [[o1]], so it’s accessible to students
  * simply via `import o1.*`. */
object Direction:

  private class Named(override val toString: String, dx: Double, dy: Double) extends Direction(dx, dy)

  /** the “straight upwards” direction with a `dx` of zero, and a `dy` of -1 */
  val Up: Direction = Named("Direction.Up", 0, -1)
  /** the “straight downwards” direction with a `dx` of zero, and a `dy` of +1 */
  val Down: Direction = Named("Direction.Down", 0,  1)
  /** the “straight leftwards” direction with a `dx` of -1, and a `dy` of zero */
  val Left: Direction = Named("Direction.Left", -1, 0)
  /** the “straight rightwards” direction with a `dx` of +1, and a `dy` of zero */
  val Right: Direction = Named("Direction.Right", 1,  0)

  /** a “non-direction” whose `dx` and `dy` are both zero */
  val NoDirection: Direction = Named("(no direction)", 0, 0)


  /** Takes in an angle as radians and returns the corresponding [[Direction]]. Zero radians
    * points rightwards; positive angles are counterclockwise from there, negative ones clockwise. */
  def fromRadians(angle: Double): Direction = Direction(cos(angle), -sin(angle))

  /** Takes in an angle as degrees and returns the corresponding [[Direction]]. Zero degrees
    * points rightwards; positive angles are counterclockwise from there, negative ones clockwise. */
  def fromDegrees(angle: Double): Direction = fromRadians(angle.toRadians)

  /** Takes in a difference between x coordinates and a difference between y coordinates and
    * constructs a [[Direction]] from them.
    *
    * @param dx  a difference between two x coordinates; it is *not* necessary that this is normalized
    *            between -1.0 and +1.0 like the `dx` and `dy` of the resulting [[Direction]] object
    * @param dy  a difference between two x coordinates; it is *not* necessary that this is normalized
    *            between -1.0 and +1.0 like the `dx` and `dy` of the resulting [[Direction]] object
    * @return the [[Direction]] that corresponds to the given deltas. If both are exactly equal to zero, returns [[NoDirection]]. */
  def fromDeltas(dx: Double, dy: Double): Direction =
    if dx == 0.0 && dy == 0.0 then NoDirection else fromRadians(atan2(-dy, dx))


  /** Returns a randomly chosen direction (evenly distributed over 360 degrees). */
  def random(): Direction =
    Direction.fromDegrees(scala.util.Random.nextInt(360))


  private type Key = scala.swing.event.Key.Value
  private val  Key = scala.swing.event.Key
  private val ArrowToDir = Map(Key.Up -> Up, Key.Left -> Left, Key.Down-> Down, Key.Right-> Right)
  private val WASDToDir  = Map(Key.W  -> Up, Key.A    -> Left, Key.S   -> Down, Key.D    -> Right)
  private val KeyToDir   = ArrowToDir ++ WASDToDir

  /** Returns the [[Direction]] that corresponds to the given arrow key. For example, the right arrow corresponds to `Right`.
    * @param key  any key on the keyboard
    * @return one of [[Up]], [[Down]], [[Left]], [[Right]]; `None` if the given key was not one of the four arrow keys
    * @see [[fromWASD]], [[fromKey]] */
  def fromArrowKey(key: Key): Option[Direction] = ArrowToDir.get(key)

  /** Returns the [[Direction]] that corresponds to the given WASD key. For example, the D key corresponds to `East`.
    * @param key  any key on the keyboard
    * @return one of [[Up]], [[Down]], [[Left]], [[Right]]; `None` if the given key was not one of the four WASD keys
    * @see [[fromArrowKey]], [[fromKey]] */
  def fromWASD(key: Key): Option[Direction] = WASDToDir.get(key)

  /** Returns the [[Direction]] that corresponds to the given arrow key. For example, the right arrow or the D key corresponds to `East`.
    * @param key  any key on the keyboard
    * @return one of [[Up]], [[Down]], [[Left]], [[Right]]; `None` if the given key was not one of the four arrow keys or one of the four WASD keys
    * @see [[fromArrowKey]], [[fromWASD]] */
  def fromKey(key: Key): Option[Direction] = KeyToDir.get(key)

end Direction

