// Copyright (c) 2011 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.wip;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.chromium.sdk.DebugEventListener.VmStatusListener;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.TabDebugEventListener;
import org.chromium.sdk.internal.BaseCommandProcessor;
import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
import org.chromium.sdk.internal.websocket.WsConnection;
import org.chromium.sdk.internal.wip.protocol.BasicConstants;
import org.chromium.sdk.internal.wip.protocol.WipParserAccess;
import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse;
import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse.Success;
import org.chromium.sdk.internal.wip.protocol.input.WipEvent;
import org.chromium.sdk.internal.wip.protocol.input.WipEventType;
import org.chromium.sdk.internal.wip.protocol.input.debugger.BreakpointResolvedEventData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.PausedEventData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.ResumedEventData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.ScriptParsedEventData;
import org.chromium.sdk.internal.wip.protocol.input.page.FrameDetachedEventData;
import org.chromium.sdk.internal.wip.protocol.input.page.FrameNavigatedEventData;
import org.chromium.sdk.internal.wip.protocol.output.WipParams;
import org.chromium.sdk.internal.wip.protocol.output.WipParamsWithResponse;
import org.chromium.sdk.internal.wip.protocol.output.WipRequest;
import org.chromium.sdk.util.GenericCallback;
import org.json.simple.JSONObject;

/**
 * Responsible for the basic processing and dispatching all incoming and outgoing messages.
 */
class WipCommandProcessor {
  private static final Logger LOGGER = Logger.getLogger(WipCommandProcessor.class.getName());

  private final WipTabImpl tabImpl;
  private final BaseCommandProcessor<Integer, JSONObject, JSONObject, WipCommandResponse>
      baseProcessor;
  private final AtomicInteger currentSeq = new AtomicInteger(0);

  WipCommandProcessor(WipTabImpl tabImpl, WsConnection wsSocket) {
    this.tabImpl = tabImpl;

    WipMessageTypeHandler handler = new WipMessageTypeHandler();

    baseProcessor =
        new BaseCommandProcessor<Integer, JSONObject, JSONObject, WipCommandResponse>(handler);
  }

  RelayOk sendRaw(JSONObject message, WipCommandCallback callback, SyncCallback syncCallback) {
    return baseProcessor.send(message, false, callback, syncCallback);
  }

  RelayOk send(WipParams params, WipCommandCallback callback, SyncCallback syncCallback) {
    WipRequest request = new WipRequest(params);
    return sendRaw(request, callback, syncCallback);
  }

  /**
   * @param <RESPONSE> type of response expected that is determined by params
   * @param params request parameters that also holds a method name
   * @param callback a callback that accepts method-specific response or null
   * @param syncCallback may be null
   */
  <RESPONSE> RelayOk send(final WipParamsWithResponse<RESPONSE> params,
      final GenericCallback<RESPONSE> callback, SyncCallback syncCallback) {
    WipRequest request = new WipRequest(params);

    WipCommandCallback commandCallback;
    if (callback == null) {
      commandCallback = null;
    } else {
      commandCallback = new WipCommandCallback.Default() {
        @Override
        protected void onSuccess(Success success) {
          RESPONSE response;
          try {
            response = params.parseResponse(success.data(), WipParserAccess.get());
          } catch (JsonProtocolParseException e) {
            throw new RuntimeException(e);
          }
          callback.success(response);
        }

        @Override
        protected void onError(String message) {
          callback.failure(new Exception(message));
        }
      };
    }

    return sendRaw(request, commandCallback, syncCallback);
  }

  void acceptResponse(JSONObject message) {
    baseProcessor.processIncoming(message);
  }

  void processEos() {
    baseProcessor.processEos();
  }

  private void processEvent(JSONObject jsonObject) {
    WipEvent event;
    try {
      event = WipParserAccess.get().parseWipEvent(jsonObject);
    } catch (JsonProtocolParseException e) {
      LOGGER.log(Level.SEVERE, "Failed to parse event", e);
      return;
    }
    EVENT_MAP.handleEvent(event, this);
  }

  /**
   * Handles all operations specific to Wip messages.
   */
  private class WipMessageTypeHandler implements
      BaseCommandProcessor.Handler<Integer, JSONObject, JSONObject, WipCommandResponse> {
    @Override
    public Integer getUpdatedSeq(JSONObject message) {
      Integer seq = currentSeq.addAndGet(1);
      message.put(BasicConstants.Property.ID, seq);
      return seq;
    }

    @Override
    public String getCommandName(JSONObject message) {
      return (String) message.get(BasicConstants.Property.METHOD);
    }

    @Override
    public void send(JSONObject message, boolean isImmediate) {
      try {
        WipCommandProcessor.this.tabImpl.getWsSocket().sendTextualMessage(
            message.toJSONString());
      } catch (IOException e) {
        LOGGER.log(Level.SEVERE, "Failed to send", e);
      }
    }

    @Override
    public WipCommandResponse parseWithSeq(JSONObject incoming) {
      if (!incoming.containsKey(BasicConstants.Property.ID)) {
        return null;
      }
      try {
        return WipParserAccess.get().parseWipCommandResponse(incoming);
      } catch (JsonProtocolParseException e) {
        throw new RuntimeException("Failed to parse response", e);
      }
    }

    @Override
    public Integer getSeq(WipCommandResponse incomingWithSeq) {
      Object seqObject = incomingWithSeq.id();
      if (seqObject == null) {
        return null;
      }
      Number number = (Number) seqObject;
      return number.intValue();
    }

    @Override
    public void acceptNonSeq(JSONObject incoming) {
      processEvent(incoming);
    }

    @Override
    public void reportVmStatus(String currentRequest, int numberOfEnqueued) {
      TabDebugEventListener tabEventListener = tabImpl.getDebugListener();
      VmStatusListener vmStatusListener =
          tabEventListener.getDebugEventListener().getVmStatusListener();
      if (vmStatusListener != null) {
        vmStatusListener.busyStatusChanged(currentRequest, numberOfEnqueued);
      }
    }
  }

  private abstract static class EventHandler<T> {
    abstract void accept(T eventData, WipCommandProcessor commandProcessor);
  }

  private static final EventMap EVENT_MAP;
  static {
    EVENT_MAP = new EventMap();

    EVENT_MAP.add(PausedEventData.TYPE, new EventHandler<PausedEventData>() {
      @Override
      void accept(PausedEventData data, WipCommandProcessor commandProcessor) {
        commandProcessor.tabImpl.getContextBuilder().createContext(data);
      }
    });
    EVENT_MAP.add(ResumedEventData.TYPE, new EventHandler<ResumedEventData>() {
      @Override
      void accept(ResumedEventData event, WipCommandProcessor commandProcessor) {
        commandProcessor.tabImpl.getContextBuilder().onResumeReportedFromRemote(event);
      }
    });
    EVENT_MAP.add(ScriptParsedEventData.TYPE, new EventHandler<ScriptParsedEventData> () {
      @Override
      void accept(ScriptParsedEventData eventData,
          WipCommandProcessor commandProcessor) {
        commandProcessor.tabImpl.getScriptManager().scriptIsReportedParsed(eventData);
      }
    });
    EVENT_MAP.add(BreakpointResolvedEventData.TYPE,
        new EventHandler<BreakpointResolvedEventData> () {
      @Override
      void accept(BreakpointResolvedEventData eventData,
          WipCommandProcessor commandProcessor) {
        commandProcessor.tabImpl.getBreakpointManager().breakpointReportedResolved(eventData);
      }
    });

    EVENT_MAP.add(FrameNavigatedEventData.TYPE, new EventHandler<FrameNavigatedEventData> () {
      @Override
      void accept(FrameNavigatedEventData eventData,
          WipCommandProcessor commandProcessor) {
        commandProcessor.tabImpl.getFrameManager().frameNavigated(eventData);
      }
    });

    EVENT_MAP.add(FrameDetachedEventData.TYPE, null);
  }

  public RelayOk runInDispatchThread(Runnable runnable, SyncCallback syncCallback) {
    return this.tabImpl.getWsSocket().runInDispatchThread(runnable, syncCallback);
  }

  private static class EventMap {
    private final Map<String, InternalHandler<?>> map = new HashMap<String, InternalHandler<?>>();

    public <T> void add(WipEventType<T> type, EventHandler<T> eventHandler) {
      InternalHandler<T> internalHandler = new InternalHandler<T>(eventHandler, type);
      map.put(type.getMethodName(), internalHandler);
    }

    public void handleEvent(WipEvent event, WipCommandProcessor commandProcessor) {
      String method = event.method();
      InternalHandler<?> parser = map.get(method);
      if (parser == null) {
        LOGGER.log(Level.INFO, "Unsupported event: " + method);
        return;
      }
      parser.handle(event, commandProcessor);
    }

    private static class InternalHandler<T> {
      private final EventHandler<T> handler;
      private final WipEventType<T> type;

      InternalHandler(EventHandler<T> handler, WipEventType<T> type) {
        this.handler = handler;
        this.type = type;
      }

      public void handle(WipEvent event, WipCommandProcessor commandProcessor) {
        if (handler == null) {
          return;
        }
        WipEvent.Data genericData = event.data();
        T data;
        if (genericData == null) {
          data = null;
        } else {
          try {
            data = type.parse(WipParserAccess.get(), genericData.getUnderlyingObject());
          } catch (JsonProtocolParseException e) {
            throw new RuntimeException(e);
          }
        }
        handler.accept(data, commandProcessor);
      }
    }
  }
}
