// 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.lattice.grid.application;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Queue;
import java.util.UUID;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.vlingo.actors.Actor;
import io.vlingo.actors.ActorInstantiator;
import io.vlingo.actors.Address;
import io.vlingo.actors.Definition;
import io.vlingo.actors.Returns;
import io.vlingo.common.SerializableConsumer;
import io.vlingo.lattice.grid.application.message.Answer;
import io.vlingo.lattice.grid.application.message.Deliver;
import io.vlingo.lattice.grid.application.message.Encoder;
import io.vlingo.lattice.grid.application.message.Forward;
import io.vlingo.lattice.grid.application.message.Message;
import io.vlingo.lattice.grid.application.message.Relocate;
import io.vlingo.lattice.grid.application.message.Start;
import io.vlingo.lattice.grid.application.message.serialization.FSTEncoder;
import io.vlingo.lattice.util.OutBuffers;
import io.vlingo.wire.fdx.outbound.ApplicationOutboundStream;
import io.vlingo.wire.message.RawMessage;
import io.vlingo.wire.node.Id;

public class OutboundGridActorControl extends Actor implements GridActorControl.Outbound {

  private static final Logger logger = LoggerFactory.getLogger(OutboundGridActorControl.class);

  private final Id localNodeId;
  private ApplicationOutboundStream stream;
  private final Encoder encoder;
  private final BiConsumer<UUID, Returns<?>> correlation;

  private final OutBuffers outBuffers;


  public OutboundGridActorControl(
          final Id localNodeId,
          final Encoder encoder,
          final BiConsumer<UUID, Returns<?>> correlation,
          final OutBuffers outBuffers) {

    this(localNodeId, null, encoder, correlation, outBuffers);
  }

  public OutboundGridActorControl(
          final Id localNodeId,
          final ApplicationOutboundStream stream,
          final Encoder encoder,
          final BiConsumer<UUID, Returns<?>> correlation,
          final OutBuffers outBuffers) {

    this.localNodeId = localNodeId;
    this.stream = stream;
    this.encoder = encoder;
    this.correlation = correlation;
    this.outBuffers = outBuffers;
  }

  @Override
  public void disburse(final Id id) {
    final Queue<Runnable> buffer = outBuffers.queue(id);
    logger.debug("Disbursing buffered messages");
    Runnable next;
    do {
      next = buffer.poll();
      if (next != null) {
        next.run();
      }
    } while (next != null);
  }

  private void send(final Id recipient, final Message message) {
    logger.debug("Buffering message {} to {}", message, recipient);
    outBuffers.enqueue(recipient, () -> {
      logger.debug("Sending message {} to {}", message, recipient);
      byte[] payload = encoder.encode(message);
      RawMessage raw = RawMessage.from(
          localNodeId.value(), -1, payload.length);
      raw.putRemaining(ByteBuffer.wrap(payload));
      stream.sendTo(raw, recipient);
    });
  }

  @Override
  public <T> void start(
          final Id recipient,
          final Id sender,
          final Class<T> protocol,
          final Address address,
          final Definition.SerializationProxy definitionProxy) {

    send(recipient, new Start<>(protocol, address, definitionProxy));
  }

  @Override
  public <T> void deliver(
          final Id recipient,
          final Id sender,
          final Returns<?> returns,
          final Class<T> protocol,
          final Address address,
          final Definition.SerializationProxy definitionProxy,
          final SerializableConsumer<T> consumer,
          final String representation) {

    final Deliver<T> deliver;
    if (returns == null) {
      deliver = new Deliver<>(protocol, address, definitionProxy, consumer, representation);
    } else {
      final UUID answerCorrelationId = UUID.randomUUID();
      deliver = new Deliver<>(protocol, address, definitionProxy, consumer, answerCorrelationId, representation);
      correlation.accept(answerCorrelationId, returns);
    }
    send(recipient, deliver);
  }

  @Override
  public <T> void answer(final Id receiver, final Id sender, final Answer<T> answer) {
    send(receiver, answer);
  }

  @Override
  public void forward(final Id receiver, final Id sender, final Message message) {
    send(receiver, new Forward(sender, message));
  }

  @Override
  public void relocate(
          final Id receiver,
          final Id sender,
          final Definition.SerializationProxy definitionProxy,
          final Address address,
          final Object snapshot, List<? extends io.vlingo.actors.Message> pending) {

    final List<Deliver<?>> messages =
            pending
              .stream()
              .map(Deliver.from(correlation))
              .collect(Collectors.toList());

    send(receiver, new Relocate(address, definitionProxy, snapshot, messages));
  }

  @Override
  public void useStream(ApplicationOutboundStream outbound) {
    this.stream = outbound;
  }

  public static class OutboundGridActorControlInstantiator implements ActorInstantiator<OutboundGridActorControl> {
    private static final long serialVersionUID = 8987209018742138417L;

    private final Id id;
    private final FSTEncoder fstEncoder;
    private final BiConsumer<UUID, Returns<?>> correlation;
    private final OutBuffers outBuffers;

    public OutboundGridActorControlInstantiator(
            final Id id,
            final FSTEncoder fstEncoder,
            final BiConsumer<UUID, Returns<?>> correlation,
            final OutBuffers outBuffers) {
      this.id = id;
      this.fstEncoder = fstEncoder;
      this.correlation = correlation;
      this.outBuffers = outBuffers;
    }

    @Override
    public OutboundGridActorControl instantiate() {
      return new OutboundGridActorControl(id, fstEncoder, correlation, outBuffers);
    }
  }
}