/*
       Licensed to the Apache Software Foundation (ASF) under one
       or more contributor license agreements.  See the NOTICE file
       distributed with this work for additional information
       regarding copyright ownership.  The ASF licenses this file
       to you under the Apache License, Version 2.0 (the
       "License"); you may not use this file except in compliance
       with the License.  You may obtain a copy of the License at

         http://www.apache.org/licenses/LICENSE-2.0

       Unless required by applicable law or agreed to in writing,
       software distributed under the License is distributed on an
       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
       KIND, either express or implied.  See the License for the
       specific language governing permissions and limitations
       under the License.
*/

package com.zsxsoft.cordova.x5;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.util.Log;
import android.view.View;

import com.tencent.smtt.sdk.WebSettings;
import com.tencent.smtt.sdk.WebSettings.LayoutAlgorithm;
import com.tencent.smtt.sdk.WebView;

import org.apache.cordova.CordovaBridge;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.ICordovaCookieManager;
import org.apache.cordova.NativeToJsMessageQueue;
import org.apache.cordova.PluginManager;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


/**
 * Glue class between CordovaWebView (main Cordova logic) and SystemWebView (the actual View).
 * We make the Engine separate from the actual View so that:
 * A) We don't need to worry about WebView methods clashing with CordovaWebViewEngine methods
 * (e.g.: goBack() is void for WebView, and boolean for CordovaWebViewEngine)
 * B) Separating the actual View from the Engine makes API surfaces smaller.
 * Class uses two-phase initialization. However, CordovaWebView is responsible for calling .init().
 */
public class X5WebViewEngine implements CordovaWebViewEngine {
  public static final String TAG = "X5WebViewEngine";

  protected final X5WebView webView;
  protected final X5CookieManager cookieManager;
  protected CordovaPreferences preferences;
  protected CordovaBridge bridge;
  protected Client client;
  protected CordovaWebView parentWebView;
  protected CordovaInterface cordova;
  protected PluginManager pluginManager;
  protected CordovaResourceApi resourceApi;
  protected NativeToJsMessageQueue nativeToJsMessageQueue;
  private BroadcastReceiver receiver;

  /**
   * Used when created via reflection.
   */
  public X5WebViewEngine(Context context, CordovaPreferences preferences) {
    this(new X5WebView(context), preferences);
  }

  public X5WebViewEngine(X5WebView webView) {
    this(webView, null);
  }

  public X5WebViewEngine(X5WebView webView, CordovaPreferences preferences) {
    this.preferences = preferences;
    this.webView = webView;
    cookieManager = new X5CookieManager(webView);
  }

  @Override
  public void init(CordovaWebView parentWebView, CordovaInterface cordova, Client client,
                   CordovaResourceApi resourceApi, PluginManager pluginManager,
                   NativeToJsMessageQueue nativeToJsMessageQueue) {
    if (this.cordova != null) {
      throw new IllegalStateException();
    }
    // Needed when prefs are not passed by the constructor
    if (preferences == null) {
      preferences = parentWebView.getPreferences();
    }
    this.parentWebView = parentWebView;
    this.cordova = cordova;
    this.client = client;
    this.resourceApi = resourceApi;
    this.pluginManager = pluginManager;
    this.nativeToJsMessageQueue = nativeToJsMessageQueue;
    webView.init(this, cordova);

    initWebViewSettings();

    nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode(new NativeToJsMessageQueue.OnlineEventsBridgeMode.OnlineEventsBridgeModeDelegate() {
      @Override
      public void setNetworkAvailable(boolean value) {
        webView.setNetworkAvailable(value);
      }

      @Override
      public void runOnUiThread(Runnable r) {
        X5WebViewEngine.this.cordova.getActivity().runOnUiThread(r);
      }
    }));
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)
      nativeToJsMessageQueue.addBridgeMode(new NativeToJsMessageQueue.EvalBridgeMode(this, cordova));
    bridge = new CordovaBridge(pluginManager, nativeToJsMessageQueue);
    exposeJsInterface(webView, bridge);
  }

  @Override
  public CordovaWebView getCordovaWebView() {
    return parentWebView;
  }

  @Override
  public ICordovaCookieManager getCookieManager() {
    return cookieManager;
  }

  @Override
  public View getView() {
    return webView;
  }

  @SuppressLint({"NewApi", "SetJavaScriptEnabled"})
  @SuppressWarnings("deprecation")
  private void initWebViewSettings() {
    webView.setInitialScale(0);
    webView.setVerticalScrollBarEnabled(false);
    // Enable JavaScript
    final WebSettings settings = webView.getSettings();
    settings.setJavaScriptEnabled(true);
    settings.setJavaScriptCanOpenWindowsAutomatically(true);
    settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL);

    // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2)
    try {
      Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[]{boolean.class});

      String manufacturer = android.os.Build.MANUFACTURER;
      Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer);
      if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.HONEYCOMB &&
        android.os.Build.MANUFACTURER.contains("HTC")) {
        gingerbread_getMethod.invoke(settings, true);
      }
    } catch (NoSuchMethodException e) {
      Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8");
    } catch (IllegalArgumentException e) {
      Log.d(TAG, "Doing the NavDump failed with bad arguments");
    } catch (IllegalAccessException e) {
      Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore");
    } catch (InvocationTargetException e) {
      Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore.");
    }

    //We don't save any form data in the application
    settings.setSaveFormData(false);
    settings.setSavePassword(false);

    // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist
    // while we do this
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
      settings.setAllowUniversalAccessFromFileURLs(true);
    }
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
      settings.setMediaPlaybackRequiresUserGesture(false);
    }
    // Enable database
    // We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16
    String databasePath = webView.getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
    settings.setDatabaseEnabled(true);
    settings.setDatabasePath(databasePath);


    //Determine whether we're in debug or release mode, and turn on Debugging!
    ApplicationInfo appInfo = webView.getContext().getApplicationContext().getApplicationInfo();
    if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 &&
      android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
      enableRemoteDebugging();
    }

    settings.setGeolocationDatabasePath(databasePath);

    // Enable DOM storage
    settings.setDomStorageEnabled(true);

    // Enable built-in geolocation
    settings.setGeolocationEnabled(true);

    // Enable AppCache
    // Fix for CB-2282
    settings.setAppCacheMaxSize(5 * 1048576);
    settings.setAppCachePath(databasePath);
    settings.setAppCacheEnabled(true);

    // Fix for CB-1405
    // Google issue 4641
    String defaultUserAgent = settings.getUserAgentString();

    // Fix for CB-3360
    String overrideUserAgent = preferences.getString("OverrideUserAgent", null);
    if (overrideUserAgent != null) {
      settings.setUserAgentString(overrideUserAgent);
    } else {
      String appendUserAgent = preferences.getString("AppendUserAgent", null);
      if (appendUserAgent != null) {
        settings.setUserAgentString(defaultUserAgent + " " + appendUserAgent);
      }
    }
    // End CB-3360

    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
    if (this.receiver == null) {
      this.receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
          settings.getUserAgentString();
        }
      };
      webView.getContext().registerReceiver(this.receiver, intentFilter);
    }
    // end CB-1405
  }

  @TargetApi(Build.VERSION_CODES.KITKAT)
  private void enableRemoteDebugging() {
    try {
      WebView.setWebContentsDebuggingEnabled(true);
    } catch (IllegalArgumentException e) {
      Log.d(TAG, "You have one job! To turn on Remote Web Debugging! YOU HAVE FAILED! ");
      e.printStackTrace();
    }
  }

  private static void exposeJsInterface(WebView webView, CordovaBridge bridge) {
    if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) {
      Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old.");
      // Bug being that Java Strings do not get converted to JS strings automatically.
      // This isn't hard to work-around on the JS side, but it's easier to just
      // use the prompt bridge instead.
      return;
    }
    X5ExposedJsApi exposedJsApi = new X5ExposedJsApi(bridge);
    webView.addJavascriptInterface(exposedJsApi, "_cordovaNative");
  }

  @Override
  public void evaluateJavascript(String js, android.webkit.ValueCallback<String> callback) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
      webView.evaluateJavascript(js, (com.tencent.smtt.sdk.ValueCallback<String>) callback);
    } else {
      Log.d(TAG, "This webview is using the old bridge");
    }
  }

  /**
   * Load the url into the webview.
   */
  @Override
  public void loadUrl(final String url, boolean clearNavigationStack) {
    webView.loadUrl(url);
  }

  @Override
  public String getUrl() {
    return webView.getUrl();
  }

  @Override
  public void stopLoading() {
    webView.stopLoading();
  }

  @Override
  public void clearCache() {
    webView.clearCache(true);
  }

  @Override
  public void clearHistory() {
    webView.clearHistory();
  }

  @Override
  public boolean canGoBack() {
    return webView.canGoBack();
  }

  /**
   * Go to previous page in history.  (We manage our own history)
   *
   * @return true if we went back, false if we are already at top
   */
  @Override
  public boolean goBack() {
    // Check webview first to see if there is a history
    // This is needed to support curPage#diffLink, since they are added to parentEngine's history, but not our history url array (JQMobile behavior)
    if (webView.canGoBack()) {
      webView.goBack();
      return true;
    }
    return false;
  }

  @Override
  public void setPaused(boolean value) {
    if (value) {
      webView.pauseTimers();
    } else {
      webView.resumeTimers();
    }
  }

  @Override
  public void destroy() {
    webView.chromeClient.destroyLastDialog();
    webView.destroy();
    // unregister the receiver
    if (receiver != null) {
      try {
        webView.getContext().unregisterReceiver(receiver);
      } catch (Exception e) {
        Log.e(TAG, "Error unregistering configuration receiver: " + e.getMessage(), e);
      }
    }
  }
}