// 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.journal.inmemory;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import io.vlingo.actors.Definition;
import io.vlingo.actors.Stoppable;
import io.vlingo.actors.World;
import io.vlingo.common.Completes;
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.Result;
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.dispatch.inmemory.InMemoryDispatcherControlDelegate;
import io.vlingo.symbio.store.journal.Journal;
import io.vlingo.symbio.store.journal.JournalReader;
import io.vlingo.symbio.store.journal.StreamReader;

public class InMemoryJournal<T,RS extends State<?>> implements Journal<T>, Stoppable {
  private final EntryAdapterProvider entryAdapterProvider;
  private final StateAdapterProvider stateAdapterProvider;
  private final List<Entry<T>> journal;
  private final Map<String,JournalReader<? extends Entry<?>>> journalReaders;
  private final Map<String,StreamReader<T>> streamReaders;
  private final Map<String, Map<Integer,Integer>> streamIndexes;
  private final Map<String,RS> snapshots;
  private final List<Dispatchable<Entry<T>, RS>> dispatchables;
  private final List<Dispatcher<Dispatchable<Entry<T>,RS>>> dispatchers;
  private final DispatcherControl dispatcherControl;

  @SuppressWarnings({ "rawtypes", "unchecked" })
  public InMemoryJournal(
          final List<Dispatcher<Dispatchable<Entry<T>,RS>>> dispatchers,
          final World world,
          final long checkConfirmationExpirationInterval,
          final long confirmationExpiration) {

    this.entryAdapterProvider = EntryAdapterProvider.instance(world);
    this.stateAdapterProvider = StateAdapterProvider.instance(world);
    this.journal = new ArrayList<>();
    this.journalReaders = new HashMap<>(1);
    this.streamReaders = new HashMap<>(1);
    this.streamIndexes = new HashMap<>();
    this.snapshots = new HashMap<>();

    this.dispatchers = dispatchers;
    this.dispatchables = new CopyOnWriteArrayList<>();
    final InMemoryDispatcherControlDelegate<Entry<T>, RS> dispatcherControlDelegate = new InMemoryDispatcherControlDelegate<>(dispatchables);

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

  public InMemoryJournal(final Dispatcher<Dispatchable<Entry<T>, RS>> dispatcher, final World world ) {
    this(Arrays.asList(dispatcher), world, 1000L, 1000L);
  }

  @Override
  public <S, ST> void append(final String streamName, final int streamVersion, final Source<S> source, final Metadata metadata,
          final AppendResultInterest interest, final Object object) {
    final Entry<T> entry = entryAdapterProvider.asEntry(source, streamVersion, metadata);
    insert(streamName, streamVersion, entry);
    dispatch(streamName, streamVersion, Collections.singletonList(entry), null);
    interest.appendResultedIn(Success.of(Result.Success), streamName, streamVersion, source, Optional.empty(), object);
  }

  @Override
  public <S, ST> void appendWith(final String streamName, final int streamVersion, final Source<S> source, final Metadata metadata, final ST snapshot,
          final AppendResultInterest interest, final Object object) {
    final Entry<T> entry = entryAdapterProvider.asEntry(source, streamVersion, metadata);
    insert(streamName, streamVersion, entry);
    final RS raw;
    final Optional<ST> snapshotResult;
    if (snapshot != null) {
      raw = stateAdapterProvider.asRaw(streamName, snapshot, streamVersion);
      snapshots.put(streamName, raw);
      snapshotResult = Optional.of(snapshot);
    } else {
      raw = null;
      snapshotResult = Optional.empty();
    }

    dispatch(streamName, streamVersion, Collections.singletonList(entry), raw);
    interest.appendResultedIn(Success.of(Result.Success), streamName, streamVersion, source, snapshotResult, object);
  }


  @Override
  public <S, ST> void appendAll(final String streamName, final int fromStreamVersion, final List<Source<S>> sources, final Metadata metadata,
          final AppendResultInterest interest, final Object object) {
    final List<Entry<T>> entries = entryAdapterProvider.asEntries(sources, fromStreamVersion, metadata);
    insert(streamName, fromStreamVersion, entries);

    dispatch(streamName, fromStreamVersion, entries, null);
    interest.appendAllResultedIn(Success.of(Result.Success), streamName, fromStreamVersion, sources, Optional.empty(), object);
  }


  @Override
  public <S, ST> void appendAllWith(final String streamName, final int fromStreamVersion, final List<Source<S>> sources,
          final Metadata metadata, final ST snapshot, final AppendResultInterest interest, final Object object) {
    final List<Entry<T>> entries = entryAdapterProvider.asEntries(sources, fromStreamVersion, metadata);
    insert(streamName, fromStreamVersion, entries);
    final RS raw;
    final Optional<ST> snapshotResult;
    if (snapshot != null) {
      raw = stateAdapterProvider.asRaw(streamName, snapshot, fromStreamVersion);
      snapshots.put(streamName, raw);
      snapshotResult = Optional.of(snapshot);
    } else {
      raw = null;
      snapshotResult = Optional.empty();
    }

    dispatch(streamName, fromStreamVersion, entries, raw);
    interest.appendAllResultedIn(Success.of(Result.Success), streamName, fromStreamVersion, sources, snapshotResult, object);
  }

  @Override
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public <ET extends Entry<?>> Completes<JournalReader<ET>> journalReader(final String name) {
    JournalReader<?> reader = journalReaders.get(name);
    if (reader == null) {
      reader = new InMemoryJournalReader(journal, name);
      journalReaders.put(name, reader);
    }
    return Completes.withSuccess((JournalReader<ET>) reader);
  }

  @Override
  @SuppressWarnings({ "unchecked", "rawtypes" })
  public Completes<StreamReader<T>> streamReader(final String name) {
    StreamReader<T> reader = streamReaders.get(name);
    if (reader == null) {
      reader = new InMemoryStreamReader(journal, streamIndexes, snapshots, name);
      streamReaders.put(name, reader);
    }
    return Completes.withSuccess(reader);
  }

  @Override
  public void conclude() {

  }

  @Override
  public boolean isStopped() {
    return false;
  }

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

  private void insert(final String streamName, final int streamVersion, final Entry<T> entry) {
    final int entryIndex = journal.size();
    final String id = "" + (entryIndex + 1);
    ((BaseEntry<T>) entry).__internal__setId(id); //questionable cast
    journal.add(entry);

    final Map<Integer, Integer> versionIndexes = streamIndexes.computeIfAbsent(streamName, k -> new HashMap<>());
    versionIndexes.put(streamVersion, entryIndex);
  }

  private void insert(final String streamName, final int fromStreamVersion, final List<Entry<T>> entries) {
    int index = 0;
    for (final Entry<T> entry : entries) {
      insert(streamName, fromStreamVersion + index, entry);
      ++index;
    }
  }

  private void dispatch(final String streamName, final int streamVersion, final List<Entry<T>> entries, final RS snapshot){
    final String id = getDispatchId(streamName, streamVersion, entries);
    final Dispatchable<Entry<T>, RS> dispatchable = new Dispatchable<>(id,  LocalDateTime.now(), snapshot, entries);
    this.dispatchables.add(dispatchable);
    this.dispatchers.forEach(d -> d.dispatch(dispatchable));
  }

  private static <T> String getDispatchId(final String streamName, final int streamVersion, final Collection<Entry<T>> entries) {
    return streamName + ":" + streamVersion + ":"
            + entries.stream().map(Entry::id).collect(Collectors.joining(":"));
  }
}