package example.akkawschat.cli

import akka.actor.ActorSystem

import akka.stream.scaladsl.{ Flow, Source }
import akka.http.scaladsl.model.Uri
import shared.Protocol

import scala.util.{ Failure, Success }

object ChatCLI extends App {
  def promptForName(): String = {
    Console.out.print("What's your name? ")
    Console.out.flush()
    Console.in.readLine()
  }

  val endpointBase = "ws://localhost:8080/chat"
  val name = promptForName()

  val endpoint = Uri(endpointBase).withQuery(Uri.Query("name" -> name))

  implicit val system = ActorSystem()
  import system.dispatcher

  import Console._
  def formatCurrentMembers(members: Seq[String]): String =
    s"(${members.size} people chatting: ${members.map(m ⇒ s"$YELLOW$m$RESET").mkString(", ")})"

  object ChatApp extends ConsoleDSL[String] {
    type State = Seq[String] // current chat members
    def initialState: Seq[String] = Nil

    def run(): Unit = {
      lazy val initialCommands =
        Command.PrintLine("Welcome to the Chat!") ~ readLineAndEmitLoop

      val inputFlow =
        Flow[Protocol.Message]
          .map {
            case Protocol.ChatMessage(sender, message) ⇒ Command.PrintLine(s"$YELLOW$sender$RESET: $message")
            case Protocol.Joined(member, all)          ⇒ Command.PrintLine(s"$YELLOW$member$RESET ${GREEN}joined!$RESET ${formatCurrentMembers(all)}") ~ Command.SetState(all)
            case Protocol.Left(member, all)            ⇒ Command.PrintLine(s"$YELLOW$member$RESET ${RED}left!$RESET ${formatCurrentMembers(all)}") ~ Command.SetState(all)
          }
          // inject initial commands before the commands generated by the server
          .prepend(Source.single(initialCommands))

      val appFlow =
        inputFlow
          .via(consoleHandler)
          .filterNot(_.trim.isEmpty)
          .watchTermination()((_, f) => f onComplete {
            case Success(_) =>
              println("\nFinishing...")
              system.terminate()
            case Failure(e) ⇒
              println(s"Connection to $endpoint failed because of '${e.getMessage}'")
              system.terminate()
          })

      println("Connecting... (Use Ctrl-D to exit.)")
      ChatClient.connect(endpoint, appFlow)
    }

    val basePrompt = s"($name) >"

    lazy val readLineAndEmitLoop: Command =
      readWithParticipantNameCompletion { line ⇒
        Command.Emit(line) ~ readLineAndEmitLoop
      }

    def readWithParticipantNameCompletion(andThen: String ⇒ Command): Command = {
      import Command._

      /** Base mode: just collect characters */
      def simpleMode(line: String): Command =
        SetPrompt(s"$basePrompt $line") ~
          SetInputHandler {
            case '\r'                       ⇒ andThen(line)
            case '@'                        ⇒ mentionMode(line, "")
            case x if x >= 0x20 && x < 0x7e ⇒ simpleMode(line + x)
            case 127 /* backspace */ ⇒
              // TODO: if backspacing to the end of a mention, enable mention mode
              simpleMode(line.dropRight(1))
          }

      /** Mention mode: collect characters and try to make progress towards one of the current candidates */
      def mentionMode(prefix: String, namePrefix: String): Command = {
        def candidates(state: State) = state.filter(_.toLowerCase startsWith namePrefix.toLowerCase).filterNot(_ == name)
        def fullLine = s"$prefix@$namePrefix"

        StatefulPrompt { state ⇒
          val cs = candidates(state)

          val completion =
            cs.size match {
              case 1          ⇒ cs.head.drop(namePrefix.size)
              case 0          ⇒ " (no one there with this name)"
              case n if n < 5 ⇒ s" (${cs.mkString(", ")})"
              case n          ⇒ s" (${cs.size} chatters)"
            }

          val left = if (completion.nonEmpty) TTY.cursorLeft(completion.length) else ""

          s"$basePrompt $prefix${TTY.GRAY}@$namePrefix$RESET${YELLOW}$completion$left$RESET"
        } ~
          SetStatefulInputHandler { state ⇒
            {
              //case '\r' ⇒ andThen(fullLine)
              case ' ' ⇒ simpleMode(fullLine + " ")
              case '\t' ⇒
                val cs = candidates(state)
                if (cs.size != 1) mentionMode(prefix, namePrefix) // ignore
                else simpleMode(prefix + "@" + cs.head + " ")
              case x if x >= 0x20 && x < 0x7e ⇒ mentionMode(prefix, namePrefix + x)
              case 127 /* backspace */ ⇒
                if (namePrefix.isEmpty) simpleMode(prefix)
                else mentionMode(prefix, namePrefix.dropRight(1))
            }
          }
      }

      simpleMode("")
    }

  }
  ChatApp.run()
}