/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * license agreements; and to You under the Apache License, version 2.0:
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * This file is part of the Apache Pekko project, which was derived from Akka.
 */

/*
 * Copyright (C) 2019-2022 Lightbend Inc. <https://www.lightbend.com>
 */

package jdocs.org.apache.pekko.persistence.typed;

import static jdocs.org.apache.pekko.persistence.typed.WebStoreCustomerFSM.*;

import java.time.Duration;
import org.apache.pekko.actor.typed.ActorRef;
import org.apache.pekko.actor.typed.javadsl.TimerScheduler;
import org.apache.pekko.persistence.typed.*;
import org.apache.pekko.persistence.typed.javadsl.*;

@Deprecated
public class PersistentFsmToTypedMigrationCompileOnlyTest {

  // #commands
  interface Command {}

  public static class AddItem implements Command {
    public final Item item;

    public AddItem(Item item) {
      this.item = item;
    }
  }

  public static class GetCurrentCart implements Command {
    public final ActorRef<ShoppingCart> replyTo;

    public GetCurrentCart(ActorRef<ShoppingCart> replyTo) {
      this.replyTo = replyTo;
    }
  }

  public enum Buy implements Command {
    INSTANCE
  }

  public enum Leave implements Command {
    INSTANCE
  }

  private enum Timeout implements Command {
    INSTANCE
  }

  // #commands

  // #state
  abstract static class State {
    public final ShoppingCart cart;

    protected State(ShoppingCart cart) {
      this.cart = cart;
    }
  }

  public static class LookingAround extends State {
    public LookingAround(ShoppingCart cart) {
      super(cart);
    }
  }

  public static class Shopping extends State {
    public Shopping(ShoppingCart cart) {
      super(cart);
    }
  }

  public static class Inactive extends State {
    public Inactive(ShoppingCart cart) {
      super(cart);
    }
  }

  public static class Paid extends State {
    public Paid(ShoppingCart cart) {
      super(cart);
    }
  }

  // #state

  // #event-adapter
  public static class PersistentFSMEventAdapter extends EventAdapter<DomainEvent, Object> {

    @Override
    public Object toJournal(DomainEvent domainEvent) {
      // leave events as is, can't roll back to PersistentFSM
      return domainEvent;
    }

    @Override
    public String manifest(DomainEvent event) {
      return "";
    }

    @Override
    public EventSeq<DomainEvent> fromJournal(Object event, String manifest) {
      if (event instanceof org.apache.pekko.persistence.fsm.PersistentFSM.StateChangeEvent) {
        // In this example the state transitions can be inferred from the events
        // Alternatively the StateChangeEvent can be converted to a private event if either the
        // StateChangeEvent.stateIdentifier
        // or StateChangeEvent.timeout is required
        // Many use cases have the same timeout so it can be hard coded, otherwise it cane be stored
        // in the state
        return EventSeq.empty();
      } else {
        // If using a new domain event model the conversion would happen here
        return EventSeq.single((DomainEvent) event);
      }
    }
    // #event-adapter
  }

  public static class ShoppingCartActor extends EventSourcedBehavior<Command, DomainEvent, State> {

    private static final String TIMEOUT_KEY = "state-timeout";
    private final TimerScheduler<Command> timers;

    public ShoppingCartActor(PersistenceId persistenceId, TimerScheduler<Command> timers) {
      super(persistenceId);
      this.timers = timers;
    }

    @Override
    public State emptyState() {
      return null;
    }

    @Override
    public CommandHandler<Command, DomainEvent, State> commandHandler() {
      // #command-handler
      CommandHandlerBuilder<Command, DomainEvent, State> builder = newCommandHandlerBuilder();

      builder.forStateType(LookingAround.class).onCommand(AddItem.class, this::addItem);

      builder
          .forStateType(Shopping.class)
          .onCommand(AddItem.class, this::addItem)
          .onCommand(Buy.class, this::buy)
          .onCommand(Leave.class, this::discardShoppingCart)
          .onCommand(Timeout.class, this::timeoutShopping);

      builder
          .forStateType(Inactive.class)
          .onCommand(AddItem.class, this::addItem)
          .onCommand(Timeout.class, () -> Effect().persist(OrderDiscarded.INSTANCE).thenStop());

      builder.forStateType(Paid.class).onCommand(Leave.class, () -> Effect().stop());

      builder.forAnyState().onCommand(GetCurrentCart.class, this::getCurrentCart);
      return builder.build();
    }

    // #command-handler

    private Effect<DomainEvent, State> addItem(AddItem item) {
      return Effect()
          .persist(new ItemAdded(item.item))
          .thenRun(
              () -> timers.startSingleTimer(TIMEOUT_KEY, Timeout.INSTANCE, Duration.ofSeconds(1)));
    }

    private Effect<DomainEvent, State> timeoutShopping() {
      return Effect()
          .persist(CustomerInactive.INSTANCE)
          .thenRun(
              () -> timers.startSingleTimer(TIMEOUT_KEY, Timeout.INSTANCE, Duration.ofSeconds(1)));
    }

    private Effect<DomainEvent, State> buy() {
      return Effect().persist(OrderExecuted.INSTANCE).thenRun(() -> timers.cancel(TIMEOUT_KEY));
    }

    private Effect<DomainEvent, State> discardShoppingCart() {
      return Effect().persist(OrderDiscarded.INSTANCE).thenRun(() -> timers.cancel(TIMEOUT_KEY));
    }

    private Effect<DomainEvent, State> getCurrentCart(State state, GetCurrentCart command) {
      command.replyTo.tell(state.cart);
      return Effect().none();
    }

    // #event-handler
    @Override
    public EventHandler<State, DomainEvent> eventHandler() {
      EventHandlerBuilder<State, DomainEvent> eventHandlerBuilder = newEventHandlerBuilder();

      eventHandlerBuilder
          .forStateType(LookingAround.class)
          .onEvent(ItemAdded.class, item -> new Shopping(new ShoppingCart(item.getItem())));

      eventHandlerBuilder
          .forStateType(Shopping.class)
          .onEvent(
              ItemAdded.class, (state, item) -> new Shopping(state.cart.addItem(item.getItem())))
          .onEvent(OrderExecuted.class, (state, item) -> new Paid(state.cart))
          .onEvent(OrderDiscarded.class, (state, item) -> state) // will be stopped
          .onEvent(CustomerInactive.class, (state, event) -> new Inactive(state.cart));

      eventHandlerBuilder
          .forStateType(Inactive.class)
          .onEvent(
              ItemAdded.class, (state, item) -> new Shopping(state.cart.addItem(item.getItem())))
          .onEvent(OrderDiscarded.class, (state, item) -> state); // will be stopped

      return eventHandlerBuilder.build();
    }

    // #event-handler

    @Override
    public EventAdapter<DomainEvent, ?> eventAdapter() {
      return new PersistentFSMEventAdapter();
    }

    // #signal-handler
    @Override
    public SignalHandler<State> signalHandler() {
      return newSignalHandlerBuilder()
          .onSignal(
              RecoveryCompleted.class,
              (state, signal) -> {
                if (state instanceof Shopping || state instanceof Inactive) {
                  timers.startSingleTimer(TIMEOUT_KEY, Timeout.INSTANCE, Duration.ofSeconds(1));
                }
              })
          .build();
    }

    // #signal-handler

    // #snapshot-adapter
    @Override
    public SnapshotAdapter<State> snapshotAdapter() {
      return PersistentFSMMigration.snapshotAdapter(
          (stateIdentifier, snapshot, timeout) -> {
            ShoppingCart cart = (ShoppingCart) snapshot;
            switch (stateIdentifier) {
              case "Looking Around":
                return new LookingAround(cart);
              case "Shopping":
                return new Shopping(cart);
              case "Inactive":
                return new Inactive(cart);
              case "Paid":
                return new Paid(cart);
              default:
                throw new IllegalStateException("Unexpected state identifier " + stateIdentifier);
            }
          });
    }
    // #snapshot-adapter
  }
}
