// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.internal;

import java.util.Collection;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;

/**
 * Provides basic command processor functionality: sends/receives commands/events and
 * supports callbacks for commands. It also supports status reporting for UI.
 * All operations such as sending/receiving/parsing are implemented by a {@link Handler}.
 *
 * @param <SEQ_KEY> type of command sequence number key
 * @param <OUTGOING> type of outgoing message
 * @param <INCOMING> type of incoming message
 * @param <INCOMING_WITH_SEQ> type of incomming message that is a command (has sequence number)
 */
public class BaseCommandProcessor<SEQ_KEY, OUTGOING, INCOMING, INCOMING_WITH_SEQ> {
  private static final Logger LOGGER = Logger.getLogger(BaseCommandProcessor.class.getName());

  public interface Handler<SEQ_KEY, OUTGOING, INCOMING, INCOMING_WITH_SEQ> {
    SEQ_KEY getUpdatedSeq(OUTGOING message);
    String getCommandName(OUTGOING message);
    void send(OUTGOING message, boolean isImmediate);
    INCOMING_WITH_SEQ parseWithSeq(INCOMING incoming);
    SEQ_KEY getSeq(INCOMING_WITH_SEQ incomingWithSeq);
    void acceptNonSeq(INCOMING incoming);
    void reportVmStatus(String currentRequest, int numberOfEnqueued);
  }

  /**
   * @param <INCOMING_WITH_SEQ>
   */
  public interface Callback<INCOMING_WITH_SEQ> {
    void messageReceived(INCOMING_WITH_SEQ response);
    void failure(String message);
  }

  private final CloseableMap<SEQ_KEY, CallbackEntry<INCOMING_WITH_SEQ>> callbackMap =
      CloseableMap.newLinkedMap();
  private final Handler<SEQ_KEY, OUTGOING, INCOMING, INCOMING_WITH_SEQ> handler;

  public BaseCommandProcessor(
      Handler<SEQ_KEY, OUTGOING, INCOMING, INCOMING_WITH_SEQ> handler) {
    this.handler = handler;
  }

  public RelayOk send(OUTGOING message, boolean isImmediate,
      Callback<? super INCOMING_WITH_SEQ> callback, SyncCallback syncCallback) {
    SEQ_KEY seq = handler.getUpdatedSeq(message);
    boolean callbackAdded;
    if (callback != null || syncCallback != null) {
      String commandName = handler.getCommandName(message);

      try {
        callbackMap.put(seq,
            new CallbackEntry<INCOMING_WITH_SEQ>(callback, syncCallback, commandName));
      } catch (IllegalStateException e) {
        throw new IllegalStateException("Connection is closed", e);
      }
      callbackAdded = true;
      reportVmStatus();
    } else {
      callbackAdded = false;
    }
    try {
      handler.send(message, isImmediate);
    } catch (RuntimeException e) {
      if (callbackAdded) {
        callbackMap.remove(seq);
      }
      throw e;
    }
    return WE_SENT_IT_RELAY_OK;
  }

  public void processIncoming(INCOMING incomingParsed) {
    final INCOMING_WITH_SEQ commandResponse = handler.parseWithSeq(incomingParsed);

    if (commandResponse != null) {
      SEQ_KEY key = handler.getSeq(commandResponse);
      CallbackEntry<INCOMING_WITH_SEQ> callbackEntry = callbackMap.removeIfContains(key);
      if (callbackEntry != null) {
        LOGGER.log(
            Level.FINE,
            "Request-response roundtrip: {0}ms",
            getCurrentMillis() - callbackEntry.commitMillis);
        reportVmStatus();

        CallbackCaller<Callback<? super INCOMING_WITH_SEQ>> caller =
            new CallbackCaller<Callback<? super INCOMING_WITH_SEQ>>() {
          @Override
          void call(Callback<? super INCOMING_WITH_SEQ> handlerCallback) {
            handlerCallback.messageReceived(commandResponse);
          }
        };
        try {
          callThemBack(callbackEntry, caller, key);
        } catch (RuntimeException e) {
          LOGGER.log(Level.SEVERE, "Failed to dispatch response to callback", e);
        }
      }
    } else {
      handler.acceptNonSeq(incomingParsed);
    }
  }

  public void processEos() {
    // We should call them in the order they have been submitted.
    Collection<CallbackEntry<INCOMING_WITH_SEQ>> entries = callbackMap.close().values();
    for (CallbackEntry<INCOMING_WITH_SEQ> entry : entries) {
      try {
        callThemBack(entry, failureCaller, null);
      } catch (RuntimeException e) {
        LOGGER.log(Level.SEVERE, "Failed to dispatch response to callback", e);
      }
    }
  }

  private void callThemBack(CallbackEntry<INCOMING_WITH_SEQ> callbackEntry,
      CallbackCaller<? super Callback<? super INCOMING_WITH_SEQ>> callbackCaller,
      SEQ_KEY requestSeq) {
    RuntimeException callbackException = null;
    try {
      if (callbackEntry.callback != null) {
        LOGGER.log(
            Level.FINE, "Notified debugger command callback, request_seq={0}", requestSeq);
        callbackCaller.call(callbackEntry.callback);
      }
    } catch (RuntimeException e) {
      callbackException = e;
      throw e;
    } finally {
      if (callbackEntry.syncCallback != null) {
        callbackEntry.syncCallback.callbackDone(callbackException);
      }
    }
  }

  private static abstract class CallbackCaller<CALLBACK> {
    abstract void call(CALLBACK handlerCallback);
  }

  private final CallbackCaller<Callback<?>> failureCaller = new CallbackCaller<Callback<?>>() {
    @Override
    void call(Callback<?> handlerCallback) {
      handlerCallback.failure("Connection closed");
    }
  };

  private static class CallbackEntry<INCOMING_WITH_SEQ> {
    final Callback<? super INCOMING_WITH_SEQ> callback;

    final SyncCallback syncCallback;

    final long commitMillis;

    final String requestName;

    CallbackEntry(Callback<? super INCOMING_WITH_SEQ> callback, SyncCallback syncCallback,
        String requestName) {
      this.callback = callback;
      this.commitMillis = getCurrentMillis();
      this.syncCallback = syncCallback;
      this.requestName = requestName;
    }
  }

  /**
   * @return milliseconds since the epoch
   */
  private static long getCurrentMillis() {
    return System.currentTimeMillis();
  }

  private final Object vmStatusReportMonitor = new Object();
  private void reportVmStatus() {
    // We synchronize, because one thread may be delivering obsolete message while a more
    // recent message has already been delivered by other thread.
    synchronized (vmStatusReportMonitor) {
      int size = callbackMap.size();
      CallbackEntry<?> firstEntry = callbackMap.peekFirst();
      // Those 2 variables above might be not in synch, so for a brief moment user may see
      // a wrong message (when size == 0 and firstEntry is null). This is OK.
      if (firstEntry == null) {
        handler.reportVmStatus(null, 0);
      } else {
        handler.reportVmStatus(firstEntry.requestName, size - 1);
      }
    }
  }

  private static final RelayOk WE_SENT_IT_RELAY_OK = new RelayOk() {};
}
