// 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.actors.testkit;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import io.vlingo.actors.Actor;
import io.vlingo.actors.Address;
import io.vlingo.actors.Configuration;
import io.vlingo.actors.Definition;
import io.vlingo.actors.Logger;
import io.vlingo.actors.LoggerProvider;
import io.vlingo.actors.MailboxProvider;
import io.vlingo.actors.Message;
import io.vlingo.actors.Protocols;
import io.vlingo.actors.Stage;
import io.vlingo.actors.World;
import io.vlingo.actors.plugin.mailbox.testkit.TestMailboxPlugin;

public class TestWorld implements AutoCloseable {
  public static ThreadLocal<TestWorld> Instance = new ThreadLocal<>();

  private final Map<Long, List<Message>> actorMessages;
  private final MailboxProvider mailboxProvider;
  private final World world;

  public static TestWorld start(final String name) {
    final World world = World.start(name);
    return new TestWorld(world, name);
  }

  public static synchronized TestWorld start(final String name, final java.util.Properties properties) {
    final World world = World.start(name, properties);
    return new TestWorld(world, name);
  }

  public static TestWorld start(final String name, final Configuration configuration) {
    final World world = World.start(name, configuration);
    return new TestWorld(world, name);
  }

  public static TestWorld start(final String name, final LoggerProvider loggerProvider) {
    return new TestWorld(World.start(name), name);
  }

  public static TestWorld startWith(final World world) {
    return new TestWorld(world, world.name());
  }

  public static synchronized TestWorld startWithDefaults(final String name) {
    return new TestWorld(World.start(name, Configuration.define()), name);
  }

  public <T> TestActor<T> actorFor(final Class<T> protocol, final Class<? extends Actor> type, final Object...parameters) {
    if (isTerminated()) {
      throw new IllegalStateException("vlingo/actors: Stopped.");
    }

    return world.stage().testActorFor(protocol, type, parameters);
  }

  public <T> TestActor<T> actorFor(final Class<T> protocol, final Definition definition) {
    if (world.isTerminated()) {
      throw new IllegalStateException("vlingo/actors: TestWorld has stopped.");
    }

    return world.stage().testActorFor(protocol, definition);
  }

  public Protocols actorFor(final Class<?>[] protocols, final Definition definition) {
    if (world.isTerminated()) {
      throw new IllegalStateException("vlingo/actors: TestWorld has stopped.");
    }

    return world.stage().testActorFor(protocols, definition);
  }

  public List<Message> allMessagesFor(final Address address) {
    final List<Message> all = actorMessages.get(address.id());
    
    return all == null ? new ArrayList<>() : all;
  }

  public void clearTrackedMessages() {
    actorMessages.clear();
  }

  @Override
  public void close() throws Exception {
    if (!isTerminated()) {
      terminate();
    }
  }

  public Logger defaultLogger() {
    return world.defaultLogger();
  }

  public Logger logger(final String name) {
    return world.logger(name);
  }

  public Stage stage() {
    return world.stage();
  }

  public Stage stageNamed(final String name) {
    return world.stageNamed(name);
  }

  public boolean isTerminated() {
    return world.isTerminated();
  }

  public void terminate() {
    world.terminate();
    actorMessages.clear();
    Instance.set(null);
  }

  public void track(final Message message) {
    final long id = message.actor().address().id();
    final List<Message> messages = actorMessages.computeIfAbsent(id, k -> new ArrayList<>());
    messages.add(message);
  }

  public World world() {
    return world;
  }

  protected MailboxProvider mailboxProvider() {
    return this.mailboxProvider;
  }

  private TestWorld(final World world, final String name) {
    Instance.set(this);
    this.world = world;
    this.actorMessages = new HashMap<>();
    this.mailboxProvider = new TestMailboxPlugin(this.world);
  }
}