// Copyright 2004-present Facebook. All Rights Reserved.

package com.facebook.react.modules.network;

import javax.annotation.Nullable;

import java.io.IOException;
import java.net.CookieHandler;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.ValueCallback;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.GuardedAsyncTask;
import com.facebook.react.bridge.GuardedResultAsyncTask;
import com.facebook.react.bridge.ReactContext;

/**
 * Cookie handler that forwards all cookies to the WebView CookieManager.
 *
 * This class relies on CookieManager to persist cookies to disk so cookies may be lost if the
 * application is terminated before it syncs.
 */
public class ForwardingCookieHandler extends CookieHandler {
  private static final String VERSION_ZERO_HEADER = "Set-cookie";
  private static final String VERSION_ONE_HEADER = "Set-cookie2";
  private static final String COOKIE_HEADER = "Cookie";

  // As CookieManager was synchronous before API 21 this class emulates the async behavior on <21.
  private static final boolean USES_LEGACY_STORE = Build.VERSION.SDK_INT < 21;

  private final CookieSaver mCookieSaver;
  private final ReactContext mContext;
  private @Nullable CookieManager mCookieManager;

  public ForwardingCookieHandler(ReactContext context) {
    mContext = context;
    mCookieSaver = new CookieSaver();
  }

  @Override
  public Map<String, List<String>> get(URI uri, Map<String, List<String>> headers)
      throws IOException {
    String cookies = getCookieManager().getCookie(uri.toString());
    if (TextUtils.isEmpty(cookies)) {
      return Collections.emptyMap();
    }

    return Collections.singletonMap(COOKIE_HEADER, Collections.singletonList(cookies));
  }

  @Override
  public void put(URI uri, Map<String, List<String>> headers) throws IOException {
    String url = uri.toString();
    for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
      String key = entry.getKey();
      if (key != null && isCookieHeader(key)) {
        addCookies(url, entry.getValue());
      }
    }
  }

  public void clearCookies(final Callback callback) {
    if (USES_LEGACY_STORE) {
      new GuardedResultAsyncTask<Boolean>(mContext) {
        @Override
        protected Boolean doInBackgroundGuarded() {
          getCookieManager().removeAllCookie();
          mCookieSaver.onCookiesModified();
          return true;
        }

        @Override
        protected void onPostExecuteGuarded(Boolean result) {
          callback.invoke(result);
        }
      }.execute();
    } else {
      clearCookiesAsync(callback);
    }
  }

  private void clearCookiesAsync(final Callback callback) {
    getCookieManager().removeAllCookies(
        new ValueCallback<Boolean>() {
          @Override
          public void onReceiveValue(Boolean value) {
            mCookieSaver.onCookiesModified();
            callback.invoke(value);
          }
        });
  }

  public void destroy() {
    if (USES_LEGACY_STORE) {
      getCookieManager().removeExpiredCookie();
      mCookieSaver.persistCookies();
    }
  }

  private void addCookies(final String url, final List<String> cookies) {
    if (USES_LEGACY_STORE) {
      runInBackground(
          new Runnable() {
            @Override
            public void run() {
              for (String cookie : cookies) {
                getCookieManager().setCookie(url, cookie);
              }
              mCookieSaver.onCookiesModified();
            }
          });
    } else {
      for (String cookie : cookies) {
        addCookieAsync(url, cookie);
      }
      mCookieSaver.onCookiesModified();
    }
  }

  @TargetApi(21)
  private void addCookieAsync(String url, String cookie) {
    getCookieManager().setCookie(url, cookie, null);
  }

  private static boolean isCookieHeader(String name) {
    return name.equalsIgnoreCase(VERSION_ZERO_HEADER) || name.equalsIgnoreCase(VERSION_ONE_HEADER);
  }

  private void runInBackground(final Runnable runnable) {
    new GuardedAsyncTask<Void, Void>(mContext) {
      @Override
      protected void doInBackgroundGuarded(Void... params) {
        runnable.run();
      }
    }.execute();
  }

  /**
   * Instantiating CookieManager in KitKat+ will load the Chromium task taking a 100ish ms so we
   * do it lazily to make sure it's done on a background thread as needed.
   */
  private CookieManager getCookieManager() {
    if (mCookieManager == null) {
      possiblyWorkaroundSyncManager(mContext);
      mCookieManager = CookieManager.getInstance();

      if (USES_LEGACY_STORE) {
        mCookieManager.removeExpiredCookie();
      }
    }

    return mCookieManager;
  }

  private static void possiblyWorkaroundSyncManager(Context context) {
    if (USES_LEGACY_STORE) {
      // This is to work around a bug where CookieManager may fail to instantiate if
      // CookieSyncManager has never been created. Note that the sync() may not be required but is
      // here of legacy reasons.
      CookieSyncManager syncManager = CookieSyncManager.createInstance(context);
      syncManager.sync();
    }
  }

  /**
   * Responsible for flushing cookies to disk. Flushes to disk with a maximum delay of 30 seconds.
   * This class is only active if we are on API < 21.
   */
  private class CookieSaver {
    private static final int MSG_PERSIST_COOKIES = 1;

    private static final int TIMEOUT = 30 * 1000; // 30 seconds

    private final Handler mHandler;

    public CookieSaver() {
      mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
          if (msg.what == MSG_PERSIST_COOKIES) {
            persistCookies();
            return true;
          } else {
            return false;
          }
        }
      });
    }

    public void onCookiesModified() {
      if (USES_LEGACY_STORE) {
        mHandler.sendEmptyMessageDelayed(MSG_PERSIST_COOKIES, TIMEOUT);
      }
    }

    public void persistCookies() {
      mHandler.removeMessages(MSG_PERSIST_COOKIES);
      runInBackground(
          new Runnable() {
            @Override
            public void run() {
              if (USES_LEGACY_STORE) {
                CookieSyncManager syncManager = CookieSyncManager.getInstance();
                syncManager.sync();
              } else {
                flush();
              }
            }
          });
    }

    @TargetApi(21)
    private void flush() {
      getCookieManager().flush();
    }
  }
}
