/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed 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.googlecode.android_scripting.interpreter.html;

import android.app.Activity;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.View;
import android.view.Window;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import com.googlecode.android_scripting.FileUtils;
import com.googlecode.android_scripting.Log;
import com.googlecode.android_scripting.SingleThreadExecutor;
import com.googlecode.android_scripting.event.Event;
import com.googlecode.android_scripting.event.EventObserver;
import com.googlecode.android_scripting.facade.EventFacade;
import com.googlecode.android_scripting.facade.ui.UiFacade;
import com.googlecode.android_scripting.future.FutureActivityTask;
import com.googlecode.android_scripting.interpreter.InterpreterConstants;
import com.googlecode.android_scripting.jsonrpc.JsonBuilder;
import com.googlecode.android_scripting.jsonrpc.JsonRpcResult;
import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
import com.googlecode.android_scripting.jsonrpc.RpcReceiverManager;
import com.googlecode.android_scripting.rpc.MethodDescriptor;
import com.googlecode.android_scripting.rpc.RpcError;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 */
public class HtmlActivityTask extends FutureActivityTask<Void> {

  private static final String HTTP = "http";
  private static final String ANDROID_PROTOTYPE_JS =
      "Android.prototype.%1$s = function(var_args) { "
          + "return this._call(\"%1$s\", Array.prototype.slice.call(arguments)); };";

  private static final String PREFIX = "file://";
  private static final String BASE_URL = PREFIX + InterpreterConstants.SCRIPTS_ROOT;

  private final RpcReceiverManager mReceiverManager;
  private final String mJsonSource;
  private final String mAndroidJsSource;
  private final String mAPIWrapperSource;
  private final String mUrl;
  private final JavaScriptWrapper mWrapper;
  private final HtmlEventObserver mObserver;
  private final UiFacade mUiFacade;
  private ChromeClient mChromeClient;
  private WebView mView;
  private MyWebViewClient mWebViewClient;
  private static HtmlActivityTask reference;
  private boolean mDestroyManager;

  public HtmlActivityTask(RpcReceiverManager manager, String androidJsSource, String jsonSource,
      String url, boolean destroyManager) {
    reference = this;
    mReceiverManager = manager;
    mJsonSource = jsonSource;
    mAndroidJsSource = androidJsSource;
    mAPIWrapperSource = generateAPIWrapper();
    mWrapper = new JavaScriptWrapper();
    mObserver = new HtmlEventObserver();
    mReceiverManager.getReceiver(EventFacade.class).addGlobalEventObserver(mObserver);
    mUiFacade = mReceiverManager.getReceiver(UiFacade.class);
    mUrl = url;
    mDestroyManager = destroyManager;
  }

  public RpcReceiverManager getRpcReceiverManager() {
    return mReceiverManager;
  }

  /*
   * New WebviewClient
   */
  private class MyWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      /*
       * if (Uri.parse(url).getHost().equals("www.example.com")) {
       * // This is my web site, so do not
       * override; let my WebView load the page return false; }
       * // Otherwise, the link is not for a
       * page on my site, so launch another Activity that handles URLs Intent intent = new
       * Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivity(intent);
       */
      if (!HTTP.equals(Uri.parse(url).getScheme())) {
        String source = null;
        try {
          source = FileUtils.readToString(new File(Uri.parse(url).getPath()));
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
        source =
            "<script>" + mJsonSource + "</script>" + "<script>" + mAndroidJsSource + "</script>"
                + "<script>" + mAPIWrapperSource + "</script>" + source;
        mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
      } else {
        mView.loadUrl(url);
      }
      return true;
    }
  }

  @Override
  public void onCreate() {
    mView = new WebView(getActivity());
    mView.setId(1);
    mView.getSettings().setJavaScriptEnabled(true);
    mView.addJavascriptInterface(mWrapper, "_rpc_wrapper");
    mView.addJavascriptInterface(new Object() {

      @SuppressWarnings("unused")
      public void register(String event, int id) {
        mObserver.register(event, id);
      }
    }, "_callback_wrapper");

    getActivity().setContentView(mView);
    mView.setOnCreateContextMenuListener(getActivity());
    mChromeClient = new ChromeClient(getActivity());
    mWebViewClient = new MyWebViewClient();
    mView.setWebChromeClient(mChromeClient);
    mView.setWebViewClient(mWebViewClient);
    mView.loadUrl("javascript:" + mJsonSource);
    mView.loadUrl("javascript:" + mAndroidJsSource);
    mView.loadUrl("javascript:" + mAPIWrapperSource);
    load();
  }

  private void load() {
    if (!HTTP.equals(Uri.parse(mUrl).getScheme())) {
      String source = null;
      try {
        source = FileUtils.readToString(new File(Uri.parse(mUrl).getPath()));
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
      mView.loadDataWithBaseURL(BASE_URL, source, "text/html", "utf-8", null);
    } else {
      mView.loadUrl(mUrl);
    }
  }

  @Override
  public void onDestroy() {
    mReceiverManager.getReceiver(EventFacade.class).removeEventObserver(mObserver);
    if (mDestroyManager) {
      mReceiverManager.shutdown();
    }
    mView.destroy();
    mView = null;
    reference = null;
    setResult(null);
  }

  public static void shutdown() {
    if (HtmlActivityTask.reference != null) {
      HtmlActivityTask.reference.finish();
    }
  }

  @Override
  public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
    mUiFacade.onCreateContextMenu(menu, v, menuInfo);
  }

  @Override
  public boolean onPrepareOptionsMenu(Menu menu) {
    return mUiFacade.onPrepareOptionsMenu(menu);
  }

  private String generateAPIWrapper() {
    StringBuilder wrapper = new StringBuilder();
    for (Class<? extends RpcReceiver> clazz : mReceiverManager.getRpcReceiverClasses()) {
      for (MethodDescriptor rpc : MethodDescriptor.collectFrom(clazz)) {
        wrapper.append(String.format(ANDROID_PROTOTYPE_JS, rpc.getName()));
      }
    }
    return wrapper.toString();
  }

  private class JavaScriptWrapper {
    @SuppressWarnings("unused")
    public String call(String data) throws JSONException {
      Log.v("Received: " + data);
      JSONObject request = new JSONObject(data);
      int id = request.getInt("id");
      String method = request.getString("method");
      JSONArray params = request.getJSONArray("params");
      MethodDescriptor rpc = mReceiverManager.getMethodDescriptor(method);
      if (rpc == null) {
        return JsonRpcResult.error(id, new RpcError("Unknown RPC.")).toString();
      }
      try {
        return JsonRpcResult.result(id, rpc.invoke(mReceiverManager, params)).toString();
      } catch (Throwable t) {
        Log.e("Invocation error.", t);
        return JsonRpcResult.error(id, t).toString();
      }
    }

    @SuppressWarnings("unused")
    public void dismiss() {
      Activity parent = getActivity();
      parent.finish();
    }
  }

  private class HtmlEventObserver implements EventObserver {
    private Map<String, Set<Integer>> mEventMap = new HashMap<String, Set<Integer>>();

    public void register(String eventName, Integer id) {
      if (mEventMap.containsKey(eventName)) {
        mEventMap.get(eventName).add(id);
      } else {
        Set<Integer> idSet = new HashSet<Integer>();
        idSet.add(id);
        mEventMap.put(eventName, idSet);
      }
    }

    @Override
    public void onEventReceived(Event event) {
      final JSONObject json = new JSONObject();
      try {
        json.put("data", JsonBuilder.build(event.getData()));
      } catch (JSONException e) {
        Log.e(e);
      }
      if (mEventMap.containsKey(event.getName())) {
        for (final Integer id : mEventMap.get(event.getName())) {
          getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
              mView.loadUrl(String.format("javascript:droid._callback(%d, %s);", id, json));
            }
          });
        }
      }
    }

    @SuppressWarnings("unused")
    public void dismiss() {
      Activity parent = getActivity();
      parent.finish();
    }
  }

  private class ChromeClient extends WebChromeClient {
    private final static String JS_TITLE = "JavaScript Dialog";

    private final Activity mActivity;
    private final Resources mResources;
    private final ExecutorService mmExecutor;

    public ChromeClient(Activity activity) {
      mActivity = activity;
      mResources = mActivity.getResources();
      mmExecutor = new SingleThreadExecutor();
    }

    @Override
    public void onReceivedTitle(WebView view, String title) {
      mActivity.setTitle(title);
    }

    @Override
    public void onReceivedIcon(WebView view, Bitmap icon) {
      mActivity.getWindow().requestFeature(Window.FEATURE_RIGHT_ICON);
      mActivity.getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON,
                                               new BitmapDrawable(mActivity.getResources(), icon));
    }

    @Override
    public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
      uiFacade.dialogCreateAlert(JS_TITLE, message);
      uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));

      mmExecutor.execute(new Runnable() {

        @Override
        public void run() {
          try {
            uiFacade.dialogShow();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
          uiFacade.dialogGetResponse();
          result.confirm();
        }
      });
      return true;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
      uiFacade.dialogCreateAlert(JS_TITLE, message);
      uiFacade.dialogSetPositiveButtonText(mResources.getString(android.R.string.ok));
      uiFacade.dialogSetNegativeButtonText(mResources.getString(android.R.string.cancel));

      mmExecutor.execute(new Runnable() {

        @Override
        public void run() {
          try {
            uiFacade.dialogShow();
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
          Map<String, Object> mResultMap = (Map<String, Object>) uiFacade.dialogGetResponse();
          if ("positive".equals(mResultMap.get("which"))) {
            result.confirm();
          } else {
            result.cancel();
          }
        }
      });

      return true;
    }

    @Override
    public boolean onJsPrompt(WebView view, String url, final String message,
        final String defaultValue, final JsPromptResult result) {
      final UiFacade uiFacade = mReceiverManager.getReceiver(UiFacade.class);
      mmExecutor.execute(new Runnable() {
        @Override
        public void run() {
          String value = null;
          try {
            value = uiFacade.dialogGetInput(JS_TITLE, message, defaultValue);
          } catch (InterruptedException e) {
            throw new RuntimeException(e);
          }
          if (value != null) {
            result.confirm(value);
          } else {
            result.cancel();
          }
        }
      });
      return true;
    }
  }
}
