/**
 * Copyright (c) 2015-present, Facebook, Inc. All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the LICENSE file in the root
 * directory of this source tree. An additional grant of patent rights can be found in the PATENTS
 * file in the same directory.
 */

package com.facebook.react.bridge;

import javax.annotation.Nullable;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

import android.os.Handler;
import android.os.Looper;

import com.facebook.common.logging.FLog;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.ws.WebSocket;
import okhttp3.ws.WebSocketCall;
import okhttp3.ws.WebSocketListener;
import okio.Buffer;

/**
 * A wrapper around WebSocketClient that recognizes packager's message format.
 */
public class JSPackagerWebSocketClient implements WebSocketListener {
  private static final String TAG = "JSPackagerWebSocketClient";

  private static final int RECONNECT_DELAY_MS = 2000;

  private final String mUrl;
  private final Handler mHandler;
  private boolean mSuppressConnectionErrors;

  public interface JSPackagerCallback {
    void onMessage(String target, String action);
  }

  private @Nullable WebSocket mWebSocket;
  private @Nullable JSPackagerCallback mCallback;

  public JSPackagerWebSocketClient(String url, JSPackagerCallback callback) {
    super();
    mUrl = url;
    mCallback = callback;
    mHandler = new Handler(Looper.getMainLooper());
  }

  public void connect() {
    OkHttpClient  httpClient = new OkHttpClient.Builder()
        .connectTimeout(10, TimeUnit.SECONDS)
        .writeTimeout(10, TimeUnit.SECONDS)
        .readTimeout(0, TimeUnit.MINUTES) // Disable timeouts for read
        .build();

    Request request = new Request.Builder().url(mUrl).build();
    WebSocketCall call = WebSocketCall.create(httpClient, request);
    call.enqueue(this);
  }

  private void reconnect() {
    if (!mSuppressConnectionErrors) {
      FLog.w(TAG, "Couldn't connect to packager, will silently retry");
      mSuppressConnectionErrors = true;
    }
    mHandler.postDelayed(
      new Runnable() {
        @Override
        public void run() {
          connect();
        }
      }, RECONNECT_DELAY_MS);
  }

  public void closeQuietly() {
    if (mWebSocket != null) {
      try {
        mWebSocket.close(1000, "End of session");
      } catch (IOException e) {
        // swallow, no need to handle it here
      }
      mWebSocket = null;
    }
  }

  private void triggerMessageCallback(String target, String action) {
    if (mCallback != null) {
      mCallback.onMessage(target, action);
    }
  }

  @Override
  public void onMessage(ResponseBody response) throws IOException {
    if (response.contentType() != WebSocket.TEXT) {
      FLog.w(TAG, "Websocket received unexpected message with payload of type " + response.contentType());
      return;
    }

    String message = null;
    try {
      message = response.source().readUtf8();
    } finally {
      response.close();
    }

    try {
      JsonParser parser = new JsonFactory().createParser(message);

      Integer version = null;
      String target = null;
      String action = null;

      while (parser.nextToken() != JsonToken.END_OBJECT) {
        String field = parser.getCurrentName();
        if ("version".equals(field)) {
          parser.nextToken();
          version = parser.getIntValue();
        } else if ("target".equals(field)) {
          parser.nextToken();
          target = parser.getText();
        } else if ("action".equals(field)) {
          parser.nextToken();
          action = parser.getText();
        }
      }
      if (version != 1) {
        return;
      }
      if (target == null || action == null) {
        return;
      }

      triggerMessageCallback(target, action);
    } catch (IOException e) {
      abort("Parsing response message from websocket failed", e);
    }
  }

  @Override
  public void onFailure(IOException e, Response response) {
    if (mWebSocket != null) {
      abort("Websocket exception", e);
    }
    reconnect();
  }

  @Override
  public void onOpen(WebSocket webSocket, Response response) {
    mWebSocket = webSocket;
    mSuppressConnectionErrors = false;
  }

  @Override
  public void onClose(int code, String reason) {
    mWebSocket = null;
    reconnect();
  }

  @Override
  public void onPong(Buffer payload) {
    // ignore
  }

  private void abort(String message, Throwable cause) {
    FLog.e(TAG, "Error occurred, shutting down websocket connection: " + message, cause);
    closeQuietly();
  }
}
