/**
 * 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.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;

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 final String mUrl;

  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;
  }

  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() {
    new Timer().schedule(new TimerTask() {
      @Override
      public void run() {
        connect();
      }
    }, 2000);
  }

  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) {
    abort("Websocket exception", e);
    reconnect();
  }

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

  @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();
  }
}

@@ -420,79 +420,81 @@
 til.
-Timer;%0Aimport java.util.TimerTask;%0Aimport java.util.concurrent.TimeUnit
+concurrent.TimeUnit;%0A%0Aimport android.os.Handler;%0Aimport android.os.Looper
 ;%0A%0Ai
@@ -1056,17 +1056,16 @@
 tener %7B%0A
-%0A
   privat
@@ -1125,35 +1125,170 @@
 t%22;%0A
-  private final String mUrl
+%0A  private static final int RECONNECT_DELAY_MS = 2000;%0A%0A  private final String mUrl;%0A  private final Handler mHandler;%0A  private boolean mSuppressConnectionErrors
 ;%0A%0A 
@@ -1607,16 +1607,68 @@
 llback;%0A
+    mHandler = new Handler(Looper.getMainLooper());%0A
   %7D%0A%0A  p
@@ -2125,47 +2125,211 @@
     
-new Timer().schedule(new TimerTask
+if (!mSuppressConnectionErrors) %7B%0A      FLog.w(TAG, %22Couldn't connect to packager, will silently retry%22);%0A      mSuppressConnectionErrors = true;%0A    %7D%0A    mHandler.postDelayed(%0A      new Runnable
 () %7B%0A
+  
     
@@ -2346,16 +2346,18 @@
 e%0A      
+  
 public v
@@ -2376,16 +2376,18 @@
         
+ 
+ 
 connect(
@@ -2399,21 +2399,39 @@
     
+  
 %7D%0A    
+ 
+ 
 %7D, 
-2000
+RECONNECT_DELAY_MS
 );%0A 
@@ -4189,24 +4189,56 @@
 response) %7B%0A
+    if (mWebSocket != null) %7B%0A  
     abort(%22W
@@ -4258,24 +4258,30 @@
 ption%22, e);%0A
+    %7D%0A
     reconnec
@@ -4393,16 +4393,55 @@
 Socket;%0A
+    mSuppressConnectionErrors = false;%0A
   %7D%0A%0A  @

Make packager reconnect less noisy