import { set, isFunction, cacheFactory, isFacWebview, lt, get, getPlatform, Platform, appVersion } from 'para-utils';
import { INtvCallback, INtvCallbackResult } from '../types/INtvDtoProtocol';
import { parseCallBackDataFromNative, buildBridgeParameter } from '../utils/bridgeUtils';

const webviewGroupCache = {};

function generateUniqueName(ntvMethod): string {
  if (!webviewGroupCache[ntvMethod]) {
    webviewGroupCache[ntvMethod] = cacheFactory(ntvMethod);
  }
  return webviewGroupCache[ntvMethod]();
}

export abstract class BridgePlugin<T, K>{
  protected param?: T;
  // bridge api的唯一标识
  protected pluginName: string;

  constructor(param?: T) {
    this.param = param;
  }

  /**
   * 当前bridge对应的pluginName，和ios/android之间的约定
   */
  protected get ntvPluginName(): string {
    throw new Error('Method Not Implement');
  }

  /**
   * 当前bridge插件最低可运行的Native版本支持.
   */
  protected get availableMinVersion(): string {
    throw new Error('Method Not Implement');
  }

  protected get ntvVersion() {
    return appVersion();
  }

  /**
   * 当前的平台类型 Android, Ios, Webview, Broswer
   */
  protected get platform(): Platform {
    return getPlatform();
  }

  /**
   * Chrome浏览器运行时容器：可以实现在浏览器或者其他平台运行时执行逻辑，弥补平台调用差异性
   * @param pluginName 真实传递到Native的插件的函数调用名字window._ntv_bar_set_navbar_1
   */
  protected abstract bridgeExecChrome(pluginName: string): void;

  protected bridgeExecIos(url): void {
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = url;
    const docContent = document.body || document.documentElement;
    docContent.appendChild(iframe);
    setTimeout(() => {
      if (iframe && iframe.parentNode) {
        iframe.parentNode.removeChild(iframe);
      }
    }, 200);
  }

  /**
   * 插件调用失败，需要清理已经绑定的错误的注册回调函数
   * 调用端需要catch error，纠正，并重新执行绑定注册流程（插件重新初始化)
   */
  private clearBridgeMemory(pluginName) {
    if (pluginName) {
      set(window, pluginName, null);
    }
  }

  /**
   * 检查当前的运行时环境是否能正常支持当前的plugin去执行native插件调用
   */
  protected canInvokeNtv(): boolean {
    if (this.platform !== Platform.FacIosWebview && this.platform !== Platform.FacAndroidWebview) {
      return false;
    }
    // native version >= plugin required min version
    if (lt(this.ntvVersion, this.availableMinVersion)) {
      throw new Error(`当前版本不支持此方法 当前版本:V${this.ntvVersion}, 插件要求最新版本${this.availableMinVersion}`);
    }
    // Android: 是否当前WEBVIEW已经存在此方法签名的定义
    if (this.platform === Platform.FacAndroidWebview && !get(window, this.pluginName)) {
      throw new Error(`当前版本此调用的ntv方法签名不存在 ${this.pluginName}`);
    }
    return true;
  }

  /**
   * 包装Native回调函数全局window对象的自动签名 如: _ntv_bar_set_navbar_1
   * 回调客户端签名可能是 window._ntv_bar_set_navbar_1();
   * Note: 同一个实例只注册一个pluginName方法签名.如果已经注册，则返回已有的。这意味着同一页面调用多次bridge，实际使用的是同一个实例
   * @param pluginName 注册的插件名
   * @param callbackDto 回调客户端的执行逻辑
   * @returns {string} cbUniqueName 返回生成的唯一的插件签名
   */
  protected registWebviewCb(pluginName: string, callbackDto): string {
    const cbUniqueName = generateUniqueName(pluginName);
    set(window, cbUniqueName, callbackDto);
    return cbUniqueName;
  }

  protected buildNtvParameter(pluginName?): string {
    return buildBridgeParameter(this.param, pluginName);
  }

  /**
   * 调用iOS Bridge执行机制
   * @param param bridge调用入参
   */
  protected calliOSEasyJsBridge(param: string) {
    const argStr = ':s' + encodeURIComponent(':' + encodeURIComponent(param));
    this.bridgeExecIos('easy-js:' + encodeURIComponent(this.pluginName) + argStr);
  }

  /**
   * Webview运行时容器：执行bridge Ntv调用, 默认在webview里面运行，
   * Note:提供其他模拟器环境（如chrome浏览器），需要自己根据情况实现
   * @param pluginName 注册的插件名
   */
  protected bridgeExec(pluginName?: string): void {
    // Android: webview只能调window对象，不能申明自定义变量，再调用
    if (this.canInvokeNtv()) {
      const ntvEncodedParameters = this.buildNtvParameter(pluginName);
      if (this.platform === Platform.FacAndroidWebview) {
        // 针对Android 的实现
        window[this.pluginName](ntvEncodedParameters);
      } else if (this.platform === Platform.FacIosWebview) {
        // 针对ios 的实现
        this.calliOSEasyJsBridge(ntvEncodedParameters);
      }
    }
    if (!isFacWebview()) {
      // 可针对不同运行环境实现特定的调用逻辑来MOCK，弥补实现差异性.
      this.bridgeExecChrome(this.pluginName);
    }
  }

  /**
   * callbackDto 对回调的一层封装
   * todo: error 的捕获和通知
   */
  protected executeCallBack(callback?: INtvCallback<K>) {
    if (callback && isFunction(callback)) {
      const callbackDto = (...args) => {
        try {
          callback(null, parseCallBackDataFromNative<K>(args));
        } catch (error) {
          this.clearBridgeMemory(this.pluginName);
          if (callback) {
            callback(error);
          }
        }
      };
      if (!this.pluginName) {
        // 把生成唯一的函数签名绑定到this.pluginName上
        this.pluginName = this.registWebviewCb(
          this.pluginName, callbackDto
        );
      } else {
        // 如已经绑定过，更新回调函数为最新的传进来的callback
        set(window, `${this.pluginName}`, callbackDto);
      }
    }
    try {
      this.bridgeExec(this.pluginName);
    } catch (error) {
      this.clearBridgeMemory(this.pluginName);
      if (callback) {
        callback(error);
      }
    }
  }

  /**
   * 执行promise形式的回调，返回promise<K>对象类型。成功执行完后就销毁挂载在桥（全局变量）下注册的函数
   * 多次调用生成的函数签名是不同的, 即便是同一个插件实例化的invokeOnce都是生成不同的ntv回调函数签名.
   */
  protected executePromise(): Promise<INtvCallbackResult<K>> {
    return new Promise((resolve, reject) => {
      const uniquePluginName = this.registWebviewCb(this.pluginName, (...args) => {
        // 注意这里的Promise执行一次就结束了，[每次调用]对应一个pluginName
        this.clearBridgeMemory(uniquePluginName);
        try {
          // 走标准Ntv回调协议解析data callback回调参数
          resolve(parseCallBackDataFromNative<K>(args));
        } catch (error) {
          reject(error);
        }
      });

      try {
        this.bridgeExec();
      } catch (error) {
        this.clearBridgeMemory(uniquePluginName);
        reject(error);
      }
    });
  }

  /**
   * 桥（全局变量下）绑定的唯一的回调函数，可一直存在，并被多次调用执行
   * 如：setNavBar时，前端只绑定一次，但会由ios/andorid多次触发回调，
   * 从回调中获取用户的点击动作
   * @param callback 回调函数
   * @param param 如果参数传进来，将会覆盖构造器的初始化参数
   */
  public invokeBind(callback?: INtvCallback<K>, param?: T): void {
    if (param) {
      this.param = param;
    }
    this.executeCallBack(callback);
  }

  /**
   * 不需要接收Native回调，直接执行NTV 方法即可.
   * @param param 参数
   * todo: 错误处理
   */
  public invoke(param?: T): void {
    if (param) {
      this.param = param;
    }
    try {
      this.bridgeExec();
    } catch (error) {
      console.log(error);
    }
  }

  /**
   * 需要回调函数返回一个, 回调函数只执行一次
   * 如何: 调用NATIVE 发送数据请求，当回调回来被客户端接收后，会自动释放callback hook.
   * 自动清理内存
   * @param param 如果参数传进来，将会覆盖构造器的初始化参数
   */
  public invokeOnce(param?: T): Promise<INtvCallbackResult<K>> {
    if (param) {
      this.param = param;
    }
    return this.executePromise();
  }

}