////////////////// NOTE TO STUDENTS //////////////////////////
// For the purposes of our course, it’s not necessary
// that you understand or even look at the code in this file.
//////////////////////////////////////////////////////////////

package o1.carsim.gui

import o1.carsim.*
import o1.util.ifImplemented
import o1.util.nice.number.*
import CarSim.Directions
import CarSim.Pos
import Directions.Segment
import scala.collection.mutable.ArrayDeque


/** The class `CarEnhancement` represents cars using the student-created `Car` class to do it.
  * Its functionality contains some things we didn’t want to put in the car exercise.
  *
  * **NOTE TO STUDENTS: In this course, you don’t need to understand how this class works or can be used.**
  *
  * The class employs several tricks so that a student’s `Car` can be used on an actual world
  * map, where the coordinates are latitudes and longitudes on a geoid, rather than the flat,
  * two-dimensional surface assumed by the student class. The basic idea is that the class
  * calls the student method on “toy coordinates”, but then converts the results into a
  * different coordinate system.
  *
  * (This class is not really a part of the GUI *per se*, but is here as an artifact of the programming assigment.) */
class CarEnhancement(consumption: Double, tankSize: Double, fuelInTank: Double, initialCoords: Directions.Coords):
  import CarEnhancement.*

  import scala.language.adhocExtensions
  object car extends Car(consumption, tankSize, fuelInTank, Pos.Origin):
    override def fuelRatio: Double = ifImplemented { if hasFiniteFuel then super.fuelRatio else 100.0                   } getOrElse 0.0
    override def fuelRange: Double = ifImplemented { if hasFiniteFuel then super.fuelRange else Double.PositiveInfinity } getOrElse 0.0
    override def location: Pos =     ifImplemented { super.location } getOrElse Pos.Origin
    override def kmDriven: Double =  ifImplemented { super.kmDriven } getOrElse 0.0
  end car

  export this.car.kmDriven
  export this.car.fuelRatio
  export this.car.fuel

  private var geoCoords = initialCoords
  def coords: Directions.Coords = this.geoCoords

  private var reachedDestination = false
  def isAtDestination = this.reachedDestination

  def isOutOfFuel = this.hasFiniteFuel && this.car.fuelRatio < NegligibleAmountOfFuel

  def isIdle: Boolean = this.itinerary.isEmpty || this.isOutOfFuel

  private def hasFiniteFuel: Boolean = this.car.fuelConsumption > 0


  private val itinerary = ArrayDeque[Segment]()

  def findRoute(destination: String): IndexedSeq[Segment] =
    this.itinerary.clear()
    val legs = Directions.findRoute(this.geoCoords.toString, destination).toIndexedSeq
    val segments = legs.flatMap( _.segments )
    this.itinerary.appendAll(segments)
    this.reachedDestination = segments.isEmpty
    segments

  private def abortMovement(): Unit =
    this.itinerary.clear()
    this.reachedDestination = false

  def advance(): StudentDriveResult =
    this.itinerary.headOption match
      case None =>
        this.reachedDestination = true
        NoDrivingToBeDone
      case Some(segment) =>
        this.driveAlong(segment)

  private def driveAlong(segment: Segment): StudentDriveResult =

    val intendedDistance = segment.distance
    val toyStart = this.car.location
    val toyDestination = ToyDestination.next(toyStart, intendedDistance)

    val actuallyDriven =
      val kmBefore = this.kmDriven
      this.car.drive(toyDestination)
      (this.car.kmDriven - kmBefore) * 1000 atLeast 0.0

    val degreeOfSuccess =
      if intendedDistance <= NegligibleDistance then 1.0 else actuallyDriven / intendedDistance

    val expectedToyLocation = toyStart + toyStart.vectorTo(toyDestination) * degreeOfSuccess
    if this.car.location.distance(expectedToyLocation) > NegligibleErrorInStudentPos then
      this.abortMovement()
      BuggyDriveResult(intendedDistance, actuallyDriven)
    else
      val whatsLeftOfSegment = (intendedDistance - actuallyDriven) atLeast 0.0
      if whatsLeftOfSegment <= NegligibleDistance then
        this.geoCoords = segment.destination
        this.itinerary.removeHead()
        if this.itinerary.isEmpty then
          this.reachedDestination = true
        FinishedSegment(intendedDistance)
      else
        this.geoCoords = interpolateToward(this.geoCoords, segment.destination, degreeOfSuccess atMost 1.0)
        this.itinerary(0) = segment.copy(distance=whatsLeftOfSegment, origin=this.geoCoords)
        StoppedMidSegment(intendedDistance, actuallyDriven)

  end driveAlong

  // A generator for destinations in the student car’s toy coordinate system.
  // The actual coordinates are essentially meaningless; just the distance carries
  // meaning. (The direction varies between calls just so that student code has to
  // deal with a variety of directions of travel.)
  private object ToyDestination:
    import scala.math.*

    // These are radians:
    private var toyHeading = 0.0
    private val GoldenAngle = Pi * (3 - sqrt(5)) // pretty arbitrary, but might as well use this: https://en.wikipedia.org/wiki/Golden_angle

    def next(startPos: Pos, metersAway: Double): Pos =
      val kmDistance = (metersAway / 1000.0) atLeast 0.0
      this.toyHeading = (this.toyHeading + GoldenAngle) % (2 * Pi)
      val (dx, dy) = (cos(this.toyHeading) * kmDistance,
                      sin(this.toyHeading) * kmDistance)
      startPos.add(dx, dy)

  end ToyDestination

end CarEnhancement


private object CarEnhancement:

  trait StudentDriveResult(val segmentLength: Double, val actuallyDriven: Double):
    val wasSuccessful: Boolean
  trait Correctly extends StudentDriveResult:
    val wasSuccessful = true
  object NoDrivingToBeDone extends Correctly, StudentDriveResult(0.0, 0.0)
  class FinishedSegment(segmentLength: Double) extends Correctly, StudentDriveResult(segmentLength, segmentLength)
  class StoppedMidSegment(segmentLength: Double, actuallyDriven: Double) extends Correctly, StudentDriveResult(segmentLength, actuallyDriven)
  class BuggyDriveResult(segmentLength: Double, actuallyDriven: Double) extends StudentDriveResult(segmentLength, actuallyDriven):
    val wasSuccessful: Boolean = false

  private val NegligibleErrorInStudentPos = 0.01

  private val NegligibleDistance = 0.001
  private val NegligibleAmountOfFuel = 0.000001

  private def interpolateToward(from: Directions.Coords, destination: Directions.Coords, ratio: Double): Directions.Coords =
    val capped = ratio atLeast 0.0 atMost 1.0
    Directions.Coords(from.lat + (destination.lat - from.lat) * capped,
                      from.lng + (destination.lng - from.lng) * capped)

end CarEnhancement


