package io.grpc.routeguide

import java.util.concurrent.TimeUnit.NANOSECONDS
import java.util.logging.Logger

import concurrency.AtomicRef
import monix.eval.Task
import monix.reactive.Observable

class RouteGuideMonixService(features: Seq[Feature]) extends RouteGuideGrpcMonix.RouteGuide {

  val logger: Logger = Logger.getLogger(classOf[RouteGuideMonixService].getName)

  private val routeNotes: AtomicRef[Map[Point, Seq[RouteNote]]] = new AtomicRef(Map.empty)

  /**
    * Gets the {@link Feature} at the requested {@link Point}. If no feature at that location
    * exists, an unnamed feature is returned at the provided location.
    *
    * @param request the requested location for the feature.
    */
  override def getFeature(request: Point): Task[Feature] = {
    Task.eval(findFeature(request))
  }

  /**
    * Gets all features contained within the given bounding {@link Rectangle}.
    *
    * @param request          the bounding rectangle for the requested features.
    * @return the observable of the corresponding features.
    */
  override def listFeatures(request: Rectangle): Observable[Feature] = {
    val left = Math.min(request.getLo.longitude, request.getHi.longitude)
    val right = Math.max(request.getLo.longitude, request.getHi.longitude)
    val top = Math.max(request.getLo.latitude, request.getHi.latitude)
    val bottom = Math.min(request.getLo.latitude, request.getHi.latitude)

    Observable.fromIterable(
      features.filter { feature =>
        val lat = feature.getLocation.latitude
        val lon = feature.getLocation.longitude
        RouteGuideService.isValid(feature) && lon >= left && lon <= right && lat >= bottom && lat <= top

      }
    )
  }

  /**
    * Gets a stream of points, and responds with statistics about the "trip": number of points,
    * number of known features visited, total distance traveled, and total time spent.
    *
    * @param points an observable of the requested route points.
    * @return a Task containing the response summary.
    */
  override def recordRoute(points: Observable[Point]): Task[RouteSummary] =
    points.foldLeftL((RouteSummary.defaultInstance, None: Option[Point], System.nanoTime())) {
      case ((summary, previous, startTime), point) =>
        val feature = findFeature(point)
        val distance = previous.map(RouteGuideService.calcDistance(_, point)) getOrElse 0
        val updated = summary.copy(
          pointCount = summary.pointCount + 1,
          featureCount = summary.featureCount + (if (RouteGuideService.isValid(feature)) 1 else 0),
          distance = summary.distance + distance,
          elapsedTime = NANOSECONDS.toSeconds(System.nanoTime() - startTime).toInt
        )
        (updated, Some(point), startTime)
    }.map(_._1)

  /**
    * Receives a stream of message/location pairs, and responds with a stream of all previous
    * messages at each of those locations.
    *
    * @param notes an observable of requested message/location pairs.
    * @return an observable of all received messages at the notes locations
    */
  override def routeChat(notes: Observable[RouteNote]): Observable[RouteNote] =
    notes.flatMap { note =>
      addNote(note)
      Observable.fromIterable(getNotes(note.getLocation))
    }

  private def findFeature(point: Point): Feature = {
    features.find { feature =>
      feature.getLocation.latitude == point.latitude && feature.getLocation.longitude == point.longitude
    } getOrElse new Feature(location = Some(point))
  }

  private def getNotes(point: Point): Seq[RouteNote] = {
    routeNotes.get.getOrElse(point, Seq.empty)
  }

  private def addNote(note: RouteNote): Unit = {
    routeNotes.updateAndGet { notes =>
      val existingNotes = notes.getOrElse(note.getLocation, Seq.empty)
      val updatedNotes = existingNotes :+ note
      notes + (note.getLocation -> updatedNotes)
    }
  }
}