package nova.monadic_sfx.ui.components.todo import scala.concurrent.Future import scala.concurrent.duration.FiniteDuration import cats.effect.Sync import cats.effect.concurrent.Deferred import io.odin.Logger import monix.bio.Task import monix.catnap.ConcurrentChannel import monix.catnap.ConsumerF import monix.execution.Ack import monix.execution.Cancelable import monix.execution.Scheduler import monix.reactive.Observable import monix.reactive.Observer import monix.reactive.OverflowStrategy import monix.reactive.observers.Subscriber import monix.reactive.subjects.ConcurrentSubject import nova.monadic_sfx.implicits.FontIcon import nova.monadic_sfx.implicits.IconLiteral import nova.monadic_sfx.implicits.JFXListView import nova.monadic_sfx.implicits.JavaFXMonixObservables._ import nova.monadic_sfx.ui.components.todo.TodoListComponent.Add import nova.monadic_sfx.ui.components.todo.TodoListComponent.Delete import nova.monadic_sfx.ui.components.todo.TodoListComponent.Edit import scalafx.Includes._ import scalafx.beans.property.StringProperty import scalafx.collections.ObservableBuffer import scalafx.scene.control.ContextMenu import scalafx.scene.control.ListCell import scalafx.scene.control.MenuItem import scalafx.scene.control.SelectionMode import scalafx.scene.layout.HBox import scalafx.scene.text.Text import nova.monadic_sfx.ui.components.todo.Store.MonixProSubject import nova.monadic_sfx.util.IOUtils import monix.tail.Iterant case class Todo(id: Int, content: String) class TodoListView( val listView: JFXListView[Todo] = TodoListView.defaultListView, val lvObs: ObservableBuffer[Todo] = ObservableBuffer.empty ) { listView.items = lvObs } object TodoListView { def defaultListView = new JFXListView[Todo] { // cellFactory = _ => // new ListCell[Todo] { // // item.onChange((a, b, c) => ()) // overr // } contextMenu = new ContextMenu { items ++= Seq( new MenuItem { text = "delete" }, new MenuItem { text = "edit" } ) } } // import scalafx.scene.control.MultipleSelectionModel // .getOrElse(Todo(-1, "blah")) implicit class Operations[A](val sink: Observer[A]) extends AnyVal {} // def reducer( // stateC: Coeval[ObservableBuffer[Todo]], // action: TodoListComponent.Command // ) = // action match { // case Add(todo) => // for { // state <- stateC // } yield state :+ todo // // case Find(id, result) => // case Edit(id, content) => stateC // case Delete(id) => // for { // state <- stateC // } yield state.filterNot(_.id == id) // case _ => stateC // } def reducer( state: Vector[Todo], action: TodoListComponent.Command ) = action match { case Add(todo) => state :+ todo // case Find(id, result) => case Edit(id, content) => state case Delete(id) => state.filterNot(_.id == id) case _ => state } def defaultListView2: Task[ ( JFXListView[Todo], Observable[Todo], Observable[Todo] ) ] = Task.deferAction(implicit s => Store .createL[TodoListComponent.Command, Vector[Todo]]( TodoListComponent.Delete(0), Vector.empty[Todo], (s: Vector[Todo], a: TodoListComponent.Command) => reducer(s, a) -> Observable.empty ) .flatMap(store => Task { val deleteSub = ConcurrentSubject.publish[Todo] val editSub = ConcurrentSubject.publish[Todo] // store.flatMap(st => Task(st.sink)) // val deleteSub2 = // deleteSub.map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo) // val addSub = // ConcurrentSubject // .publish[Todo] // .map(todo => (buf: ObservableBuffer[Todo]) => buf :+ todo) // val state = Observable(deleteSub2, addSub).merge.scan0( // ObservableBuffer.empty[Todo] // )((buf, fn) => fn(buf)) val todos = store.map { case (_, items) => items } val listView = new JFXListView[Todo] { lv => def selectedItems = lv.selectionModel().selectedItems.view // items = todos items <-- todos // .map(ObservableBuffer.from(_)) cellFactory = _ => new ListCell[Todo] { val _text = StringProperty("") val _graphic = new HBox { children = Seq( new FontIcon { iconSize = 10 iconLiteral = IconLiteral.Gmi10k }, new Text { text <== _text } ) } item.onChange((_, _, todo) => { println("called") if (todo != null) { _text() = s"${todo.id} - ${todo.content}" graphic = _graphic } else { _text() = "" graphic = null } }) } selectionModel().selectionMode = SelectionMode.Multiple contextMenu = new ContextMenu { items ++= Seq( new MenuItem { text = "Add" onAction = _ => store.sink .onNext(TodoListComponent.Add(Todo(1, "blah3"))) }, new MenuItem { text = "Delete" // onAction = _ => // for { // items <- Option(lv.selectionModel().selectedItems) // _ <- Some(items.foreach(item => deleteSub.onNext(item))) // } yield () onAction = _ => selectedItems .map(todo => TodoListComponent.Delete(todo.id)) .foreach(store.sink.onNext) }, new MenuItem { text = "Edit" // onAction = _ => // Option(lv.selectionModel().selectedItems).foreach(items => // items.foreach(item => editSub.onNext(item)) // ) } ) } } (listView, deleteSub, editSub) } ) ) } private[todo] class TodoListComponentImpure( todoListView: TodoListView ) { def add(todo: Todo) = todoListView.lvObs += todo def find(id: Int) = todoListView.lvObs.find(_.id == id) def edit(id: Int, content: String) = find(id) .map(todo => todoListView.lvObs.replaceAll( todo, Todo(id, content) ) ) .getOrElse(false) } class TodoListOps private ( props: TodoListOps.Props ) { import props._ // lazy val internal = new TodoListComponentImpure(todoListView) // def add(todo: Todo) = Task(internal.add(todo)) def add(todo: Todo) = Task(todoListView.lvObs += todo).executeOn(fxScheduler) def find(id: Int) = Task(todoListView.lvObs.find(_.id == id)).executeOn(fxScheduler) def delete(id: Int) = (for { mbTodo <- find(id) _ <- logger.debug(mbTodo.toString()) res <- Task( mbTodo.map(todo => todoListView.lvObs.removeAll(todo)) ) _ <- logger.debug(todoListView.lvObs.toString()) } yield res.getOrElse(false)).executeOn(fxScheduler) def edit(id: Int, content: String) = (for { mbTodo <- find(id) res <- Task( mbTodo.map(todo => todoListView.lvObs.replaceAll( todo, Todo(id, content) ) ) ) } yield res.getOrElse(false)).executeOn(fxScheduler) } object TodoListOps { class Props( val todoListView: TodoListView, val fxScheduler: Scheduler, val logger: Logger[Task] ) { def create = Task(new TodoListOps(this)) } } object TodoListComponent { sealed trait Complete object Complete extends Complete sealed trait Command // sealed trait Tell extends Command // sealed abstract class Ask extends Command case class Add(todo: Todo) extends Command case class Find(id: Int, result: Deferred[Task, Option[Todo]]) extends Command case class Edit(id: Int, content: String) extends Command case class Delete(id: Int) extends Command // private case class FindInternal(id: Int, result: Deferred[Task, Todo]) // extends Ask class Props( val todoListView: TodoListView, val fxScheduler: Scheduler, val channel: ConcurrentChannel[ Task, TodoListComponent.Complete, TodoListComponent.Command ], val logger: Logger[Task] ) { def create = for { todoListOps <- new TodoListOps.Props(todoListView, fxScheduler, logger).create consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) _ <- consumer.startAndForget } yield (new TodoListComponent(this)) private def todoConsumer( consumer: ConsumerF[Task, Complete, Command], ops: TodoListOps ): Task[Unit] = consumer.pull .flatMap { case Left(complete) => logger.info("Received `Complete` event") case Right(command) => logger.debug(s"Received command $command") >> (command match { // case t: Tell => // t match { // case Add(todo) => ops.add(todo) // case _ => Task.unit // } case Add(todo) => ops.add(todo) // case Find(id) => // for { // p <- Deferred[Task, Todo] // _ <- channel.push(FindInternal(id, p)) // res <- p.get // } yield (res) case Find(id, result) => for { mbTodo <- ops.find(id) } yield result.complete(mbTodo) // case _ => Task.unit case Delete(id) => ops.delete(id) case Edit(id, content) => ops.edit(id, content) }) } .flatMap(_ => todoConsumer(consumer, ops)) } } class TodoListComponent(props: TodoListComponent.Props) { import props._ import TodoListComponent._ def send(command: Command) = channel.push(command) def ask[T]( commandBuilder: Deferred[Task, T] => Command )(implicit timeout: FiniteDuration) = for { p <- Deferred[Task, T] _ <- channel.push(commandBuilder(p)) res <- p.get.timeout(timeout) } yield res def stop = channel.halt(Complete) // import scala.concurrent.duration._ // val x = ask(FindInternal(0, _))(2.seconds) } // : F[ProSubject[A, (A, M)]] // interface Middleware { // fun dispatch(store: Store, next: (A) -> Unit, action: A) // } trait Middleware[A, M] { def dispatch[T]( store: MonixProSubject[A, (A, M)], cb: (A, M) => Task[T], cb2: (A, M) => Observable[(A, M)], cb3: Observable[(A, M)] => Observable[(A, M)] ) = { // store.fil store.mapEval { case (a, m) => IOUtils.toTask(cb(a, m)) } store.flatMap { case (a, m) => cb2(a, m) } cb3(store) def cb3impl(obs: Observable[(A, M)]) = obs.doOnNext { case (a, m) => IOUtils.toTask(Task(println("hello"))) } def cb3impl2(obs: Observable[(A, M)]) = obs.filter { case (a, m) => m == "" } cb3impl2(cb3impl(store)) val s = Seq(cb3impl _) val res = s.foldLeft(Observable.empty[(A, M)]) { case (o1, o2) => o2(o1) } val x = Iterant[Task].of(1, 2, 3) // x match { // case Next(item, rest) => () // case Halt(e) => () // } } } object Store { type Reducer[A, M] = (M, A) => (M, Observable[A]) type MonixProSubject[-I, +O] = Observable[O] with Observer[I] // class MonixProSubject2[-I, +O] extends Subject[I, O] object MonixProSubject { def from[I, O]( observer: Observer[I], observable: Observable[O] ): MonixProSubject[I, O] = new Observable[O] with Observer[I] { override def onNext(elem: I): Future[Ack] = observer.onNext(elem) override def onError(ex: Throwable): Unit = observer.onError(ex) override def onComplete(): Unit = observer.onComplete() override def unsafeSubscribeFn(subscriber: Subscriber[O]): Cancelable = observable.unsafeSubscribeFn(subscriber) } } def createL[A, M]( initialAction: A, initialState: M, reducer: Reducer[A, M], overflowStrategy: OverflowStrategy.Synchronous[A] = OverflowStrategy.DropOld(50) ) = Task.deferAction { implicit s => Task { val subject = ConcurrentSubject.publish[A](overflowStrategy) val fold: ((A, M), A) => (A, M) = { case ((_, state), action) => { val (newState, effects) = reducer(state, action) effects.subscribe(subject.onNext _) action -> newState } } MonixProSubject.from( subject, subject .scan[(A, M)](initialAction -> initialState)(fold) .behavior(initialAction -> initialState) .refCount ) } } def create[F[_], A, M]( initialAction: A, initialState: M, reducer: Reducer[A, M] )(implicit s: Scheduler, F: Sync[F]): F[Observable[(A, M)]] = F.delay { val subject = ConcurrentSubject.publish[A] val fold: ((A, M), A) => (A, M) = { case ((_, state), action) => { val (newState, effects) = reducer(state, action) effects.subscribe(subject.onNext _) action -> newState } } subject .scan[(A, M)](initialAction -> initialState)(fold) .behavior(initialAction -> initialState) .refCount } } // object TodoListComponent { // sealed trait Complete // object Complete extends Complete // sealed trait Command // class Props( // val todoListView: TodoListView, // val fxScheduler: Scheduler, // val channel: ConcurrentChannel[ // Task, // TodoListComponent.Complete, // TodoListComponent.Command // ] // ) { // def create = Task(new TodoListComponent(this)) // } // } // class TodoListComponent(props: TodoListComponent.Props) { // import props._ // import TodoListComponent._ // def init = // for { // todoListOps <- new TodoListOps.Props(todoListView, fxScheduler).create // consumer = channel.consume.use(ref => todoConsumer(ref, todoListOps)) // _ <- consumer.startAndForget // } yield () // def send(command: Command) = channel.push(command) // def todoConsumer( // consumer: ConsumerF[Task, Complete, Command], // ops: TodoListOps // ) = // consumer.pull.flatMap { // case Left(value) => Task.unit // case Right(value) => Task.unit // } // } // def askHandler( // channel: ConcurrentChannel[ // Task, // TodoListComponent.Complete, // TodoListComponent.Command // ], // consumer: ConsumerF[Task, Complete, Command], // ops: TodoListOps // ) = // consumer.pull.flatMap { // case Left(complete) => Task.unit // case Right(command) => // command match { // case a: Ask => // a match { // case Find(id) => // for { // p <- Deferred[Task, Todo] // _ <- channel.push(FindInternal(id, p)) // res <- p.get // } yield (res) // case FindInternal(id, result) => // for { // mb <- ops.find(id) // } yield result.complete(mb.get) // case _ => Task.unit // } // case _ => Task.unit // } // }