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

package jdocs.persistence;

import docs.persistence.ExampleJsonMarshaller;
import docs.persistence.proto.FlightAppModels;

import java.io.NotSerializableException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import spray.json.JsObject;

import akka.persistence.journal.EventAdapter;
import akka.persistence.journal.EventSeq;
import akka.protobufv3.internal.InvalidProtocolBufferException;
import akka.serialization.SerializerWithStringManifest;

public class PersistenceSchemaEvolutionDocTest {

  public
  // #protobuf-read-optional-model
  static enum SeatType {
    Window("W"),
    Aisle("A"),
    Other("O"),
    Unknown("");

    private final String code;

    private SeatType(String code) {
      this.code = code;
    }

    public static SeatType fromCode(String c) {
      if (Window.code.equals(c)) return Window;
      else if (Aisle.code.equals(c)) return Aisle;
      else if (Other.code.equals(c)) return Other;
      else return Unknown;
    }
  }
  // #protobuf-read-optional-model

  public
  // #protobuf-read-optional-model
  static class SeatReserved {
    public final String letter;
    public final int row;
    public final SeatType seatType;

    public SeatReserved(String letter, int row, SeatType seatType) {
      this.letter = letter;
      this.row = row;
      this.seatType = seatType;
    }
  }
  // #protobuf-read-optional-model

  public
  // #protobuf-read-optional
  /**
   * Example serializer impl which uses protocol buffers generated classes (proto.*) to perform the
   * to/from binary marshalling.
   */
  static class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
    @Override
    public int identifier() {
      return 67876;
    }

    private final String seatReservedManifest = SeatReserved.class.getName();

    @Override
    public String manifest(Object o) {
      return o.getClass().getName();
    }

    @Override
    public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
      if (seatReservedManifest.equals(manifest)) {
        // use generated protobuf serializer
        try {
          return seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes));
        } catch (InvalidProtocolBufferException e) {
          throw new IllegalArgumentException(e.getMessage());
        }
      } else {
        throw new NotSerializableException("Unable to handle manifest: " + manifest);
      }
    }

    @Override
    public byte[] toBinary(Object o) {
      if (o instanceof SeatReserved) {
        SeatReserved s = (SeatReserved) o;
        return FlightAppModels.SeatReserved.newBuilder()
            .setRow(s.row)
            .setLetter(s.letter)
            .setSeatType(s.seatType.code)
            .build()
            .toByteArray();

      } else {
        throw new IllegalArgumentException("Unable to handle: " + o);
      }
    }

    // -- fromBinary helpers --

    private SeatReserved seatReserved(FlightAppModels.SeatReserved p) {
      return new SeatReserved(p.getLetter(), p.getRow(), seatType(p));
    }

    // handle missing field by assigning "Unknown" value
    private SeatType seatType(FlightAppModels.SeatReserved p) {
      if (p.hasSeatType()) return SeatType.fromCode(p.getSeatType());
      else return SeatType.Unknown;
    }
  }
  // #protobuf-read-optional

  public static class RenamePlainJson {
    public
    // #rename-plain-json
    static class JsonRenamedFieldAdapter implements EventAdapter {
      // use your favorite json library
      private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();

      private final String V1 = "v1";
      private final String V2 = "v2";

      // this could be done independently for each event type
      @Override
      public String manifest(Object event) {
        return V2;
      }

      @Override
      public JsObject toJournal(Object event) {
        return marshaller.toJson(event);
      }

      @Override
      public EventSeq fromJournal(Object event, String manifest) {
        if (event instanceof JsObject) {
          JsObject json = (JsObject) event;
          if (V1.equals(manifest)) json = rename(json, "code", "seatNr");
          return EventSeq.single(json);
        } else {
          throw new IllegalArgumentException(
              "Can only work with JSON, was: " + event.getClass().getName());
        }
      }

      private JsObject rename(JsObject json, String from, String to) {
        // use your favorite json library to rename the field
        JsObject renamed = json;
        return renamed;
      }
    }
    // #rename-plain-json
  }

  public static class SimplestCustomSerializer {

    public
    // #simplest-custom-serializer-model
    static class Person {
      public final String name;
      public final String surname;

      public Person(String name, String surname) {
        this.name = name;
        this.surname = surname;
      }
    }
    // #simplest-custom-serializer-model

    public
    // #simplest-custom-serializer
    /**
     * Simplest possible serializer, uses a string representation of the Person class.
     *
     * <p>Usually a serializer like this would use a library like: protobuf, kryo, avro, cap'n
     * proto, flatbuffers, SBE or some other dedicated serializer backend to perform the actual
     * to/from bytes marshalling.
     */
    static class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
      private final Charset utf8 = StandardCharsets.UTF_8;

      private final String personManifest = Person.class.getName();

      // unique identifier of the serializer
      @Override
      public int identifier() {
        return 1234567;
      }

      // extract manifest to be stored together with serialized object
      @Override
      public String manifest(Object o) {
        return o.getClass().getName();
      }

      // serialize the object
      @Override
      public byte[] toBinary(Object obj) {
        if (obj instanceof Person) {
          Person p = (Person) obj;
          return (p.name + "|" + p.surname).getBytes(utf8);
        } else {
          throw new IllegalArgumentException(
              "Unable to serialize to bytes, clazz was: " + obj.getClass().getName());
        }
      }

      // deserialize the object, using the manifest to indicate which logic to apply
      @Override
      public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
        if (personManifest.equals(manifest)) {
          String nameAndSurname = new String(bytes, utf8);
          String[] parts = nameAndSurname.split("[|]");
          return new Person(parts[0], parts[1]);
        } else {
          throw new NotSerializableException(
              "Unable to deserialize from bytes, manifest was: "
                  + manifest
                  + "! Bytes length: "
                  + bytes.length);
        }
      }
    }
    // #simplest-custom-serializer
  }

  public static class SamplePayload {
    private final Object payload;

    public SamplePayload(Object payload) {
      this.payload = payload;
    }

    public Object getPayload() {
      return payload;
    }
  }

  // #split-events-during-recovery
  interface Version1 {};

  interface Version2 {}

  // #split-events-during-recovery
  public
  // #split-events-during-recovery
  // V1 event:
  static class UserDetailsChanged implements Version1 {
    public final String name;
    public final String address;

    public UserDetailsChanged(String name, String address) {
      this.name = name;
      this.address = address;
    }
  }

  // #split-events-during-recovery
  public
  // #split-events-during-recovery
  // corresponding V2 events:
  static class UserNameChanged implements Version2 {
    public final String name;

    public UserNameChanged(String name) {
      this.name = name;
    }
  }
  // #split-events-during-recovery
  public
  // #split-events-during-recovery
  static class UserAddressChanged implements Version2 {
    public final String address;

    public UserAddressChanged(String address) {
      this.address = address;
    }
  }

  // #split-events-during-recovery
  public
  // #split-events-during-recovery
  // event splitting adapter:
  static class UserEventsAdapter implements EventAdapter {
    @Override
    public String manifest(Object event) {
      return "";
    }

    @Override
    public EventSeq fromJournal(Object event, String manifest) {
      if (event instanceof UserDetailsChanged) {
        UserDetailsChanged c = (UserDetailsChanged) event;
        if (c.name == null) return EventSeq.single(new UserAddressChanged(c.address));
        else if (c.address == null) return EventSeq.single(new UserNameChanged(c.name));
        else return EventSeq.create(new UserNameChanged(c.name), new UserAddressChanged(c.address));
      } else {
        return EventSeq.single(event);
      }
    }

    @Override
    public Object toJournal(Object event) {
      return event;
    }
  }
  // #split-events-during-recovery

  public static class CustomerBlinked {
    public final long customerId;

    public CustomerBlinked(long customerId) {
      this.customerId = customerId;
    }
  }

  public
  // #string-serializer-skip-deleved-event-by-manifest
  static class EventDeserializationSkipped {
    public static EventDeserializationSkipped instance = new EventDeserializationSkipped();

    private EventDeserializationSkipped() {}
  }

  // #string-serializer-skip-deleved-event-by-manifest
  public
  // #string-serializer-skip-deleved-event-by-manifest
  static class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
    private final Charset utf8 = StandardCharsets.UTF_8;
    private final String customerBlinkedManifest = "blinked";

    // unique identifier of the serializer
    @Override
    public int identifier() {
      return 8337;
    }

    // extract manifest to be stored together with serialized object
    @Override
    public String manifest(Object o) {
      if (o instanceof CustomerBlinked) return customerBlinkedManifest;
      else return o.getClass().getName();
    }

    @Override
    public byte[] toBinary(Object o) {
      return o.toString().getBytes(utf8); // example serialization
    }

    @Override
    public Object fromBinary(byte[] bytes, String manifest) {
      if (customerBlinkedManifest.equals(manifest)) return EventDeserializationSkipped.instance;
      else return new String(bytes, utf8);
    }
  }
  // #string-serializer-skip-deleved-event-by-manifest

  public
  // #string-serializer-skip-deleved-event-by-manifest-adapter
  static class SkippedEventsAwareAdapter implements EventAdapter {
    @Override
    public String manifest(Object event) {
      return "";
    }

    @Override
    public Object toJournal(Object event) {
      return event;
    }

    @Override
    public EventSeq fromJournal(Object event, String manifest) {
      if (event == EventDeserializationSkipped.instance) return EventSeq.empty();
      else return EventSeq.single(event);
    }
  }
  // #string-serializer-skip-deleved-event-by-manifest-adapter

  // #string-serializer-handle-rename
  public
  // #string-serializer-handle-rename
  static class RenamedEventAwareSerializer extends SerializerWithStringManifest {
    private final Charset utf8 = StandardCharsets.UTF_8;

    // unique identifier of the serializer
    @Override
    public int identifier() {
      return 8337;
    }

    private final String oldPayloadClassName =
        "docs.persistence.OldPayload"; // class NOT available anymore
    private final String myPayloadClassName = SamplePayload.class.getName();

    // extract manifest to be stored together with serialized object
    @Override
    public String manifest(Object o) {
      return o.getClass().getName();
    }

    @Override
    public byte[] toBinary(Object o) {
      if (o instanceof SamplePayload) {
        SamplePayload s = (SamplePayload) o;
        return s.payload.toString().getBytes(utf8);
      } else {
        // previously also handled "old" events here.
        throw new IllegalArgumentException(
            "Unable to serialize to bytes, clazz was: " + o.getClass().getName());
      }
    }

    @Override
    public Object fromBinary(byte[] bytes, String manifest) throws NotSerializableException {
      if (oldPayloadClassName.equals(manifest)) return new SamplePayload(new String(bytes, utf8));
      else if (myPayloadClassName.equals(manifest))
        return new SamplePayload(new String(bytes, utf8));
      else throw new NotSerializableException("unexpected manifest [" + manifest + "]");
    }
  }
  // #string-serializer-handle-rename

  public
  // #detach-models
  // Domain model - highly optimised for domain language and maybe "fluent" usage
  static class Customer {
    public final String name;

    public Customer(String name) {
      this.name = name;
    }
  }

  // #detach-models
  public
  // #detach-models
  static class Seat {
    public final String code;

    public Seat(String code) {
      this.code = code;
    }

    public SeatBooked bookFor(Customer customer) {
      return new SeatBooked(code, customer);
    }
  }

  // #detach-models
  public
  // #detach-models
  static class SeatBooked {
    public final String code;
    public final Customer customer;

    public SeatBooked(String code, Customer customer) {
      this.code = code;
      this.customer = customer;
    }
  }

  // #detach-models
  public
  // #detach-models
  // Data model - highly optimised for schema evolution and persistence
  static class SeatBookedData {
    public final String code;
    public final String customerName;

    public SeatBookedData(String code, String customerName) {
      this.code = code;
      this.customerName = customerName;
    }
  }
  // #detach-models

  // #detach-models-adapter
  class DetachedModelsAdapter implements EventAdapter {
    @Override
    public String manifest(Object event) {
      return "";
    }

    @Override
    public Object toJournal(Object event) {
      if (event instanceof SeatBooked) {
        SeatBooked s = (SeatBooked) event;
        return new SeatBookedData(s.code, s.customer.name);
      } else {
        throw new IllegalArgumentException("Unsupported: " + event.getClass());
      }
    }

    @Override
    public EventSeq fromJournal(Object event, String manifest) {
      if (event instanceof SeatBookedData) {
        SeatBookedData d = (SeatBookedData) event;
        return EventSeq.single(new SeatBooked(d.code, new Customer(d.customerName)));
      } else {
        throw new IllegalArgumentException("Unsupported: " + event.getClass());
      }
    }
  }
  // #detach-models-adapter

  public
  // #detach-models-adapter-json
  static class JsonDataModelAdapter implements EventAdapter {

    // use your favorite json library
    private final ExampleJsonMarshaller marshaller = new ExampleJsonMarshaller();

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

    @Override
    public JsObject toJournal(Object event) {
      return marshaller.toJson(event);
    }

    @Override
    public EventSeq fromJournal(Object event, String manifest) {
      if (event instanceof JsObject) {
        JsObject json = (JsObject) event;
        return EventSeq.single(marshaller.fromJson(json));
      } else {
        throw new IllegalArgumentException(
            "Unable to fromJournal a non-JSON object! Was: " + event.getClass());
      }
    }
  }
  // #detach-models-adapter-json

}
