/*
 * 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 org.apache.weex.bridge;

import android.content.Intent;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.view.Menu;

import com.alibaba.fastjson.JSONArray;
import org.apache.weex.WXSDKInstance;
import org.apache.weex.WXSDKManager;
import org.apache.weex.WXSDKEngine;
import org.apache.weex.adapter.IWXUserTrackAdapter;
import org.apache.weex.common.Destroyable;
import org.apache.weex.common.WXException;
import org.apache.weex.common.WXModule;
import org.apache.weex.ui.config.ConfigModuleFactory;
import org.apache.weex.ui.module.WXDomModule;
import org.apache.weex.ui.module.WXTimerModule;
import org.apache.weex.utils.WXLogUtils;
import org.apache.weex.utils.cache.RegisterCache;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.weex.utils.cache.RegisterCache.ModuleCache;

/**
 * Manager class for weex module. There are two types of modules in weex, one is instance-level module,
 * the other is global-level module. Instance-level module will be created every time an instance
 * is created, while global-level module will be singleton in {@link WXSDKEngine}.
 */
public class WXModuleManager {

  /**
   * module class object dictionary
   */
  private static volatile ConcurrentMap<String, ModuleFactoryImpl> sModuleFactoryMap = new ConcurrentHashMap<>();
  private static Map<String, WXModule> sGlobalModuleMap = new HashMap<>();
  private static Map<String, WXDomModule> sDomModuleMap = new HashMap<>();


  /**
   * module object dictionary
   * K : instanceId, V : Modules
   */
  private static Map<String, Map<String, WXModule>> sInstanceModuleMap = new ConcurrentHashMap<>();


  public static boolean registerModule(Map<String, ModuleCache> moduleCacheMap) {
    if (moduleCacheMap.isEmpty())
      return true;

    final Iterator<Entry<String, ModuleCache>> iterator = moduleCacheMap.entrySet().iterator();
    WXBridgeManager.getInstance()
            .postWithName(new Runnable() {
              @Override
              public void run() {
                Map<String, Object> modules = new HashMap<>();

                while (iterator.hasNext()) {
                  Entry<String, ModuleCache> next = iterator.next();
                  ModuleCache value = next.getValue();
                  String moduleName = value.name;
                  if (TextUtils.equals(moduleName, WXDomModule.WXDOM)) {
                    WXLogUtils.e("Cannot registered module with name 'dom'.");
                    continue;
                  }

                  if (sModuleFactoryMap != null && sModuleFactoryMap.containsKey(moduleName)) {
                    WXLogUtils.w("WXComponentRegistry Duplicate the Module name: " + moduleName);
                  }
                  ModuleFactory factory = value.factory;
                  try {
                    registerNativeModule(moduleName, factory);
                  } catch (WXException e) {
                    WXLogUtils.e("registerNativeModule" + e);
                  }

                  if (value.global) {
                    try {
                      WXModule wxModule = factory.buildInstance();
                      wxModule.setModuleName(moduleName);
                      sGlobalModuleMap.put(moduleName, wxModule);
                    } catch (Exception e) {
                      WXLogUtils.e(moduleName + " class must have a default constructor without params. ", e);
                    }
                  }

                  try {
                    sModuleFactoryMap.put(moduleName, new ModuleFactoryImpl(factory));
                  } catch (Throwable e) {

                  }
                  modules.put(moduleName, factory.getMethods());
                }
                WXSDKManager.getInstance().registerModules(modules);
              }
            },null,"registerModule From Cache");
    return true;
  }

  /**
   * Register module to JavaScript and Android
   */
  public static boolean registerModule(final String moduleName, final ModuleFactory factory, final boolean global) throws WXException {
    if (moduleName == null || factory == null) {
      return false;
    }

    if (TextUtils.equals(moduleName,WXDomModule.WXDOM)) {
      WXLogUtils.e("Cannot registered module with name 'dom'.");
      return false;
    }

    if(RegisterCache.getInstance().cacheModule(moduleName,factory,global)) {
      return true;
    }

    //execute task in js thread to make sure register order is same as the order invoke register method.
    WXBridgeManager.getInstance()
            .postWithName(new Runnable() {
              @Override
              public void run() {
                if (sModuleFactoryMap != null && sModuleFactoryMap.containsKey(moduleName)) {
                  WXLogUtils.w("WXComponentRegistry Duplicate the Module name: " + moduleName);
                }
                try {
                  registerNativeModule(moduleName, factory);
                } catch (WXException e) {
                  WXLogUtils.e("registerNativeModule" + e);
                }

                if (global) {
                  try {
                    WXModule wxModule = factory.buildInstance();
                    wxModule.setModuleName(moduleName);
                    sGlobalModuleMap.put(moduleName, wxModule);
                  } catch (Exception e) {
                    WXLogUtils.e(moduleName + " class must have a default constructor without params. ", e);
                  }
                }

                registerJSModule(moduleName, factory);

                try {
                  sModuleFactoryMap.put(moduleName, new ModuleFactoryImpl(factory));
                } catch (Throwable e) {

                }
              }
            },null,"registerModule");
    return true;

  }

  static boolean registerNativeModule(String moduleName, ModuleFactory factory) throws WXException {
    if (factory == null) {
      return false;
    }

    try {
      if (!sModuleFactoryMap.containsKey(moduleName) ) {
        sModuleFactoryMap.put(moduleName, new ModuleFactoryImpl(factory));
      }
    }catch (ArrayStoreException e){
      e.printStackTrace();
      //ignore:
      //may throw this exception:
      //java.lang.String cannot be stored in an array of type java.util.HashMap$HashMapEntry[]

      WXLogUtils.e("[WXModuleManager] registerNativeModule Error moduleName:"  + moduleName + " Error:" + e.toString());
    }
    return true;
  }

  static boolean registerJSModule(String moduleName, ModuleFactory factory) {
    Map<String, Object> modules = new HashMap<>();
    modules.put(moduleName, factory.getMethods());
    WXSDKManager.getInstance().registerModules(modules);
    return true;
  }

  static Object callModuleMethod(final String instanceId, String moduleStr, String methodStr, JSONArray args) {
    ModuleFactory factory = sModuleFactoryMap.get(moduleStr).mFactory;
    if(factory == null){
      WXLogUtils.e("[WXModuleManager] module factory not found.");
      return null;
    }
    final WXModule wxModule = findModule(instanceId, moduleStr,factory);
    if (wxModule == null) {
      return null;
    }
    WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);
    wxModule.mWXSDKInstance = instance;

    final Invoker invoker = factory.getMethodInvoker(methodStr);
    try {
      if(instance != null) {
        IWXUserTrackAdapter userTrackAdapter = WXSDKManager.getInstance().getIWXUserTrackAdapter();
        if(userTrackAdapter != null) {
          HashMap<String, Serializable> data = new HashMap<String, Serializable>();
          data.put(IWXUserTrackAdapter.MONITOR_ERROR_CODE, "101");
          data.put(IWXUserTrackAdapter.MONITOR_ARG, moduleStr + "." + methodStr);
          data.put(IWXUserTrackAdapter.MONITOR_ERROR_MSG, instance.getBundleUrl());
          userTrackAdapter.commit(instance.getContext(), null, IWXUserTrackAdapter.INVOKE_MODULE, null, data);
        }
        return dispatchCallModuleMethod(instance,wxModule,args,invoker);
      } else {
        WXLogUtils.e("callModuleMethod >>> instance is null");
        return null;
      }
    } catch (Exception e) {
      WXLogUtils.e("callModuleMethod >>> invoke module:" + moduleStr + ", method:" + methodStr + " failed. ", e);
      return null;
    } finally {
      if (wxModule instanceof WXDomModule || wxModule instanceof WXTimerModule) {
        wxModule.mWXSDKInstance = null;
      }
    }
  }

  private static Object dispatchCallModuleMethod(@NonNull WXSDKInstance instance, @NonNull WXModule wxModule,
                                                 @NonNull JSONArray args, @NonNull Invoker invoker) throws Exception{
    if(!instance.isPreRenderMode()) {
      return instance.getNativeInvokeHelper().invoke(wxModule,invoker,args);
    }
    // we are in preRender mode
    if(invoker.isRunOnUIThread()) {/*ASYNC CALL*/
//      DOMAction moduleInvocationAction = Actions.getModuleInvocationAction(wxModule,args,invoker);
//      WXSDKManager.getInstance().getWXDomManager().postAction(instance.getInstanceId(), moduleInvocationAction,false);
      return null;
    } else {/*SYNC CALL*/
      return instance.getNativeInvokeHelper().invoke(wxModule,invoker,args);
    }
  }

  public static boolean hasModule(String module) {
    return sGlobalModuleMap.containsKey(module) || sModuleFactoryMap.containsKey(module);
  }

  private static WXModule findModule(String instanceId, String moduleStr,ModuleFactory factory) {
    // find WXModule
    WXModule wxModule = sGlobalModuleMap.get(moduleStr);

    //not global module
    if (wxModule == null) {
      Map<String, WXModule> moduleMap = sInstanceModuleMap.get(instanceId);
      if (moduleMap == null) {
        moduleMap = new ConcurrentHashMap<>();
        sInstanceModuleMap.put(instanceId, moduleMap);
      }
      // if cannot find the Module, create a new Module and save it
      wxModule = moduleMap.get(moduleStr);
      if (wxModule == null) {
        try {
          if(factory instanceof ConfigModuleFactory){
            WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);
            wxModule = ((ConfigModuleFactory) factory).buildInstance(instance);
          }else{
            wxModule = factory.buildInstance();
          }
          wxModule.setModuleName(moduleStr);
        } catch (Exception e) {
          WXLogUtils.e(moduleStr + " module build instace failed.", e);
          return null;
        }
        moduleMap.put(moduleStr, wxModule);
      }
    }

    return wxModule;
  }

  /**Hook Activity life cycle callback begin***/


  public static void onActivityCreate(String instanceId){

    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityCreate();
        } else {
          WXLogUtils.w("onActivityCreate can not find the " + key + " module");
        }
      }
    }

  }

  public static void onActivityStart(String instanceId){

    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityStart();
        } else {
          WXLogUtils.w("onActivityStart can not find the " + key + " module");
        }
      }
    }
  }

  public static void onActivityPause(String instanceId){
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityPause();
        } else {
          WXLogUtils.w("onActivityPause can not find the " + key + " module");
        }
      }
    }
  }

  public static void onActivityResume(String instanceId){
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityResume();
        } else {
          WXLogUtils.w("onActivityResume can not find the " + key + " module");
        }
      }
    }
  }

  public static void onActivityStop(String instanceId){
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityStop();
        } else {
          WXLogUtils.w("onActivityStop can not find the " + key + " module");
        }
      }
    }
  }

  public static void onActivityDestroy(String instanceId){
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityDestroy();
        } else {
          WXLogUtils.w("onActivityDestroy can not find the " + key + " module");
        }
      }
    }
  }

  public static boolean onActivityBack(String instanceId){
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          return module.onActivityBack();
        } else {
          WXLogUtils.w("onActivityCreate can not find the " + key + " module");
        }
      }
    }
    return false;
  }

  public static void onActivityResult(String instanceId,int requestCode, int resultCode, Intent data){

    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onActivityResult(requestCode, resultCode, data);
        } else {
          WXLogUtils.w("onActivityResult can not find the " + key + " module");
        }
      }
    }
  }

  public static boolean onCreateOptionsMenu(String instanceId,Menu menu) {
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onCreateOptionsMenu(menu);
        } else {
          WXLogUtils.w("onActivityResult can not find the " + key + " module");
        }
      }
    }
    return false;
  }

  public static void onRequestPermissionsResult(String instanceId ,int requestCode, String[] permissions, int[] grantResults) {
    Map<String, WXModule> modules = sInstanceModuleMap.get(instanceId);
    if(modules!=null) {
      for (String key : modules.keySet()) {
        WXModule module = modules.get(key);
        if (module != null) {
          module.onRequestPermissionsResult(requestCode, permissions, grantResults);
        } else {
          WXLogUtils.w("onActivityResult can not find the " + key + " module");
        }
      }
    }
  }

  public static void destroyInstanceModules(String instanceId) {
    sDomModuleMap.remove(instanceId);
    Map<String, WXModule> moduleMap = sInstanceModuleMap.remove(instanceId);
    if (moduleMap == null || moduleMap.size() < 1) {
      return;
    }
    Iterator<Entry<String, WXModule>> iterator = moduleMap.entrySet().iterator();
    Entry<String, WXModule> entry;
    while (iterator.hasNext()) {
      entry = iterator.next();
      WXModule module = entry.getValue();
      if(module instanceof Destroyable){
        ((Destroyable)module).destroy();
      }

    }
  }

  public static void createDomModule(WXSDKInstance instance){
    if(instance != null) {
      sDomModuleMap.put(instance.getInstanceId(), new WXDomModule(instance));
    }
  }

  public static void destoryDomModule(String instanceID){
    sDomModuleMap.remove(instanceID);
  }

  public static WXDomModule getDomModule(String instanceId){
    return sDomModuleMap.get(instanceId);
  }

  public static void reload(){
    if (sModuleFactoryMap != null && sModuleFactoryMap.size() > 0) {
      for (Map.Entry<String, ModuleFactoryImpl> entry : sModuleFactoryMap.entrySet()) {
        try {
          registerJSModule(entry.getKey(), entry.getValue().mFactory);
        } catch (Throwable e) {

        }
      }
    }
  }

  /**
   * registerWhenCreateInstance
   */
  public static void registerWhenCreateInstance(){
    if (sModuleFactoryMap != null && sModuleFactoryMap.size() > 0) {
      for (Map.Entry<String, ModuleFactoryImpl> entry : sModuleFactoryMap.entrySet()) {
        try {
          if (!entry.getValue().hasRigster) {
            registerJSModule(entry.getKey(), entry.getValue().mFactory);
          }
        } catch (Throwable e) {

        }
      }
    }
  }

  /**
   * resetAllModuleState
   */
  public static void resetAllModuleState() {
    if (sModuleFactoryMap != null && sModuleFactoryMap.size() > 0) {
      for (Map.Entry<String, ModuleFactoryImpl> entry : sModuleFactoryMap.entrySet()) {
        entry.getValue().hasRigster = false;
      }
    }
  }

  /**
   * resetModuleState
   * @param module
   * @param state
   */
  public static void resetModuleState(String module, boolean state) {
    if (sModuleFactoryMap != null && sModuleFactoryMap.size() > 0) {
      for (Map.Entry<String, ModuleFactoryImpl> entry : sModuleFactoryMap.entrySet()) {
        try {
          if (entry.getKey() != null && entry.getKey().equals(module)) {
            entry.getValue().hasRigster = state;
          }
        } catch (Throwable e) {

        }
      }
    }
  }
}
