package nova.monadic_sfx.ui.components.router import scala.concurrent.duration._ import cats.kernel.Eq import cats.syntax.eq._ import com.softwaremill.quicklens._ import io.circe.Codec import io.circe.Decoder import io.circe.Encoder import io.circe.generic.JsonCodec import io.circe.generic.semiauto._ import io.odin.Logger import monix.bio.Task import monix.eval.Coeval import monix.reactive.Observable import nova.monadic_sfx.util.History import nova.monadic_sfx.util.IOUtils import nova.monadic_sfx.util.controls.JFXSpinner import nova.monadic_sfx.util.reactive.store.Middlewares import nova.monadic_sfx.util.reactive.store.Reducer import nova.monadic_sfx.util.reactive.store.Store import scalafx.scene.Parent object FXRouter { final case class State[P](page: P, history: History[P]) object State { implicit def eqForAction[T] = Eq.fromUniversalEquals[State[T]] } sealed abstract class Action[+T] final case class Replace[T](page: T) extends Action[T] final case class HistoryEvent[T](page: T) extends Action[T] final case object Forward extends Action[Nothing] final case object Backward extends Action[Nothing] object Action { implicit def codec[T: Encoder: Decoder]: Codec[Action[T]] = deriveCodec implicit def eqForAction[T] = Eq.fromUniversalEquals[Action[T]] } type FXStore[P] = Store[Action[P], State[P]] } class FXRouter[P]()(implicit E: Encoder[P], D: Decoder[P]) { import FXRouter._ def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] = Task.deferAction(implicit s => for { mw <- Middlewares.actionLoggerMiddleware[Action[P], State[P]]( logger, "RouterStore" ) store <- Store.createL[Action[P], State[P]]( Replace(initialPage), State(initialPage, History(initialPage)), Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _), Seq(mw) // Seq(classOf[HistoryEvent[P]]) ) } yield store ) def reducer( state: State[P], action: Action[P] ): (State[P], Option[Task[Action[P]]]) = action match { // case Init => (state, None) case Replace(p) => (state.copy(page = p, history = state.history :+ p), None) case HistoryEvent(p) => (state.copy(page = p), None) case Forward => val s1 = state.modify(_.history).using(_.forward) val s2 = s1.modify(_.page).setTo(s1.history.current) s2 -> Some(Task.pure(HistoryEvent(s2.history.current))) case Backward => val s1 = state.modify(_.history).using(_.backward) val s2 = s1.modify(_.page).setTo(s1.history.current) s2 -> Some(Task.pure(HistoryEvent(s2.history.current))) } def render( resolver: P => Parent, transitionDelay: FiniteDuration = 500.millis )(implicit store: FXStore[P]) = store .filter { case (a, _) => a =!= FXRouter.Forward } .filter { case (a, _) => a =!= FXRouter.Backward } .distinctUntilChanged .flatMap { case (_, FXRouter.State(p, _)) => Observable.from(Coeval(new JFXSpinner)) ++ Observable.from( IOUtils.toTask( Task .racePair( Task.sleep(transitionDelay), Task.pure(resolver(p)) ) .flatMap { case Left(_ -> fib) => fib.join case Right(fib -> res) => fib.join >> Task.pure(res) } ) ) } def link( page: P, store: FXStore[P] ) = { store.onNext(Replace(page)) } } @JsonCodec sealed trait Page object Page { final case object Home extends Page final case object UserHome extends Page final case object Todo extends Page implicit val eqForPage = Eq.fromUniversalEquals[Page] } // case class State() // object RouterStore { // sealed trait Action // case object Init extends Action // def reducer(state: State, action: Action) = // action match { // case Init => state // } // def apply() = // Store.createL[Action, State](Init, State(), Reducer(reducer _), Seq.empty) // }