// Copyright © 2012-2020 VLINGO LABS. All rights reserved.
//
// This Source Code Form is subject to the terms of the
// Mozilla Public License, v. 2.0. If a copy of the MPL
// was not distributed with this file, You can obtain
// one at https://mozilla.org/MPL/2.0/.

package io.vlingo.symbio.store.object.inmemory;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import io.vlingo.actors.Actor;
import io.vlingo.actors.ActorInstantiator;
import io.vlingo.actors.Definition;
import io.vlingo.common.Completes;
import io.vlingo.common.Failure;
import io.vlingo.common.Success;
import io.vlingo.symbio.BaseEntry;
import io.vlingo.symbio.Entry;
import io.vlingo.symbio.EntryAdapterProvider;
import io.vlingo.symbio.Metadata;
import io.vlingo.symbio.Source;
import io.vlingo.symbio.State;
import io.vlingo.symbio.StateAdapterProvider;
import io.vlingo.symbio.store.EntryReader;
import io.vlingo.symbio.store.QueryExpression;
import io.vlingo.symbio.store.Result;
import io.vlingo.symbio.store.StorageException;
import io.vlingo.symbio.store.dispatch.Dispatchable;
import io.vlingo.symbio.store.dispatch.Dispatcher;
import io.vlingo.symbio.store.dispatch.DispatcherControl;
import io.vlingo.symbio.store.dispatch.DispatcherControl.DispatcherControlInstantiator;
import io.vlingo.symbio.store.dispatch.control.DispatcherControlActor;
import io.vlingo.symbio.store.object.ObjectStore;
import io.vlingo.symbio.store.object.ObjectStoreDelegate;
import io.vlingo.symbio.store.object.ObjectStoreEntryReader;
import io.vlingo.symbio.store.object.StateObject;
import io.vlingo.symbio.store.object.StateSources;
import io.vlingo.symbio.store.state.StateStoreEntryReader;

/**
 * In-memory implementation of {@code ObjectStore}. Note that {@code queryAll()} variations
 * do not support select constraints but always select all stored objects.
 */
public class InMemoryObjectStoreActor extends Actor implements ObjectStore {
  private final EntryAdapterProvider entryAdapterProvider;

  private final List<Dispatcher<Dispatchable<BaseEntry<?>,State<?>>>> dispatchers;
  private final DispatcherControl dispatcherControl;
  private final Map<String,StateStoreEntryReader<?>> entryReaders;

  private final ObjectStoreDelegate<BaseEntry<?>,State<?>> storeDelegate;

  /**
   * Construct my default state.
   * @param dispatcher The dispatcher to be used
   */
  public InMemoryObjectStoreActor(final Dispatcher<Dispatchable<BaseEntry<?>,State<?>>> dispatcher){
    this(Arrays.asList(dispatcher), 1000L, 1000L);
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  public InMemoryObjectStoreActor(
          final List<Dispatcher<Dispatchable<BaseEntry<?>,State<?>>>> dispatchers,
          final long checkConfirmationExpirationInterval,
          final long confirmationExpiration ) {

    this.entryAdapterProvider = EntryAdapterProvider.instance(stage().world());

    this.dispatchers = dispatchers;

    this.entryReaders = new HashMap<>();

    this.storeDelegate = new InMemoryObjectStoreDelegate(StateAdapterProvider.instance(stage().world()));

    this.dispatcherControl = stage().actorFor(
            DispatcherControl.class,
            Definition.has(
                    DispatcherControlActor.class,
                    new DispatcherControlInstantiator(
                            dispatchers,
                            this.storeDelegate,
                            checkConfirmationExpirationInterval,
                            confirmationExpiration)));
  }

  /*
   * @see io.vlingo.symbio.store.object.ObjectStore#close()
   */
  @Override
  public void close() {
    this.storeDelegate.close();
  }

  /*
   * @see io.vlingo.symbio.store.object.ObjectStoreReader#entryReader(java.lang.String)
   */
  @Override
  @SuppressWarnings("unchecked")
  public Completes<EntryReader<? extends Entry<?>>> entryReader(final String name) {
    EntryReader<? extends Entry<?>> reader = entryReaders.get(name);
    if (reader == null) {
      final Definition definition = Definition.has(InMemoryObjectStoreEntryReaderActor.class, new ObjectStoreEntryReaderInstantiator(readOnlyJournal(), name));
      reader = childActorFor(ObjectStoreEntryReader.class, definition);
    }
    return completes().with(reader);
  }

  @Override
  public <T extends StateObject, E> void persist(StateSources<T, E> stateSources, Metadata metadata, long updateId, PersistResultInterest interest, Object object) {
    try {
      final T stateObject = stateSources.stateObject();
      final List<Source<E>> sources = stateSources.sources();

      final State<?> raw = storeDelegate.persist(stateObject, updateId, metadata);

      final int entryVersion = (int) stateSources.stateObject().version();
      final List<BaseEntry<?>> entries = entryAdapterProvider.asEntries(sources, entryVersion, metadata);
      final Dispatchable<BaseEntry<?>, State<?>> dispatchable = buildDispatchable(raw, entries);

      this.storeDelegate.persistEntries(entries);
      this.storeDelegate.persistDispatchable(dispatchable);

      dispatch(dispatchable);
      interest.persistResultedIn(Success.of(Result.Success), stateObject, 1, 1, object);
    } catch (StorageException e){
      logger().error("Failed to persist all objects", e);
      interest.persistResultedIn(Failure.of(e), null, 0, 0, object);
    }
  }

  @Override
  public <T extends StateObject, E> void persistAll(Collection<StateSources<T, E>> allStateSources, Metadata metadata, long updateId, PersistResultInterest interest, Object object) {
    final Collection<T> allPersistentObjects = new ArrayList<>();
    try {
      for (StateSources<T, E> stateSources : allStateSources) {
        final T stateObject = stateSources.stateObject();
        final State<?> state = storeDelegate.persist(stateObject, updateId, metadata);
        allPersistentObjects.add(stateObject);

        final int entryVersion = (int) stateSources.stateObject().version();
        final List<BaseEntry<?>> entries = entryAdapterProvider.asEntries(stateSources.sources(), entryVersion, metadata);
        this.storeDelegate.persistEntries(entries);

        final Dispatchable<BaseEntry<?>, State<?>> dispatchable = buildDispatchable(state, entries);
        this.storeDelegate.persistDispatchable(dispatchable);

        dispatch(buildDispatchable(state, entries));
      }

      interest.persistResultedIn(Success.of(Result.Success), allPersistentObjects, allPersistentObjects.size(), allPersistentObjects.size(), object);
    } catch (final StorageException e){
      logger().error("Failed to persist all objects", e);
      interest.persistResultedIn(Failure.of(e), null, 0, 0, object);
    }
  }

  /*
   * @see io.vlingo.symbio.store.object.ObjectStore#queryAll(io.vlingo.symbio.store.object.QueryExpression, io.vlingo.symbio.store.object.ObjectStore.QueryResultInterest, java.lang.Object)
   */
  @Override
  public void queryAll(final QueryExpression expression, final QueryResultInterest interest, final Object object) {
    final QueryMultiResults queryMultiResults = this.storeDelegate.queryAll(expression);
    interest.queryAllResultedIn(Success.of(Result.Success), queryMultiResults, object);
  }

  /*
   * @see io.vlingo.symbio.store.object.ObjectStore#queryObject(io.vlingo.symbio.store.object.QueryExpression, io.vlingo.symbio.store.object.ObjectStore.QueryResultInterest, java.lang.Object)
   */
  @Override
  public void queryObject(final QueryExpression expression, final QueryResultInterest interest, final Object object) {
    final QuerySingleResult result = this.storeDelegate.queryObject(expression);

    if (result.stateObject != null) {
      interest.queryObjectResultedIn(Success.of(Result.Success), result, object);
    } else {
      interest.queryObjectResultedIn(Failure.of(new StorageException(Result.NotFound, "No object identified by expression: " + expression)), QuerySingleResult.of(null), object);
    }
  }

  @Override
  public void stop() {
    dispatcherControl.stop();
    super.stop();
  }


  private void dispatch(final Dispatchable<BaseEntry<?>, State<?>> dispatchable){
    this.dispatchers.forEach(d -> d.dispatch(dispatchable));
  }

  private static Dispatchable<BaseEntry<?>, State<?>> buildDispatchable(final State<?> state, final List<BaseEntry<?>> entries) {
    final String id = getDispatchId(state, entries);
    return new Dispatchable<>(id, LocalDateTime.now(), state, entries);
  }

  private static String getDispatchId(final State<?> raw, final List<BaseEntry<?>> entries) {
    return raw.id + ":" + entries.stream().map(Entry::id).collect(Collectors.joining(":"));
  }

  private List<BaseEntry<?>> readOnlyJournal() {
    return ((InMemoryObjectStoreDelegate) storeDelegate).readOnlyJournal();
  }

  private static class ObjectStoreEntryReaderInstantiator implements ActorInstantiator<InMemoryObjectStoreEntryReaderActor> {
    private static final long serialVersionUID = -2022300658559205459L;

    final String name;
    final List<BaseEntry<?>> readOnlyJournal;

    ObjectStoreEntryReaderInstantiator(final List<BaseEntry<?>> readOnlyJournal, final String name) {
      this.readOnlyJournal = readOnlyJournal;
      this.name = name;
    }

    @Override
    public InMemoryObjectStoreEntryReaderActor instantiate() {
      return new InMemoryObjectStoreEntryReaderActor(readOnlyJournal(), name);
    }

    @Override
    public Class<InMemoryObjectStoreEntryReaderActor> type() {
      return InMemoryObjectStoreEntryReaderActor.class;
    }

    @SuppressWarnings("unchecked")
    <E, ET extends Entry<E>> ET readOnlyJournal() {
      return (ET) readOnlyJournal;
    }
  }
}