/*
 * 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.ui.component;

import android.annotation.SuppressLint;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.ImageView;

import org.apache.weex.R;
import org.apache.weex.IWXRenderListener;
import org.apache.weex.WXEnvironment;
import org.apache.weex.WXSDKInstance;
import org.apache.weex.annotation.Component;
import org.apache.weex.common.Constants;
import org.apache.weex.common.OnWXScrollListener;
import org.apache.weex.common.WXErrorCode;
import org.apache.weex.common.WXRenderStrategy;
import org.apache.weex.instance.InstanceOnFireEventInterceptor;
import org.apache.weex.performance.WXInstanceApm;
import org.apache.weex.ui.action.BasicComponentData;
import org.apache.weex.utils.WXLogUtils;
import org.apache.weex.utils.WXUtils;
import org.apache.weex.utils.WXViewUtils;

import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import org.apache.weex.WXSDKInstance.OnInstanceVisibleListener;

@Component(lazyload = false)
public class WXEmbed extends WXDiv implements OnInstanceVisibleListener,NestedContainer {

  public static final  String STRATEGY_NONE =  "none";
  public static final  String STRATEGY_NORMAL =  "normal";
  public static final  String STRATEGY_HIGH =  "high";


  public static final  String PRIORITY_LOW =  "low";
  public static final  String PRIORITY_NORMAL =  "normal";
  public static final  String PRIORITY_HIGH =  "high";

  public static final String ITEM_ID = "itemId";

  private String src;
  protected WXSDKInstance mNestedInstance;
  private static int ERROR_IMG_WIDTH = (int) WXViewUtils.getRealPxByWidth(270,750);
  private static int ERROR_IMG_HEIGHT = (int) WXViewUtils.getRealPxByWidth(260,750);

  private boolean mIsVisible = true;
  private EmbedRenderListener mListener;
  private EmbedInstanceOnScrollFireEventInterceptor mInstanceOnScrollFireEventInterceptor;


  private String priority = PRIORITY_NORMAL;

  private String strategy = "normal";  //none, normal, high(ignore priority)

  private long hiddenTime;



  public interface EmbedManager {
    WXEmbed getEmbed(String itemId);
    void putEmbed(String itemId,WXEmbed comp);
  }

  public static class FailToH5Listener extends ClickToReloadListener {
    @SuppressLint("SetJavaScriptEnabled")
    @Override
    public void onException(NestedContainer comp, String errCode, String msg) {
      //downgrade embed
      if( errCode != null && comp instanceof WXEmbed && errCode.startsWith("1|")) {
        ViewGroup container = comp.getViewContainer();
        WebView webView = new WebView(container.getContext());
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        webView.setLayoutParams(params);
        webView.getSettings().setJavaScriptEnabled(true);

        //WebView Remote Code Execution Vulnerability
        webView.removeJavascriptInterface("searchBoxJavaBridge_");
        webView.removeJavascriptInterface("accessibility");
        webView.removeJavascriptInterface("accessibilityTraversal");
        webView.getSettings().setSavePassword(false);

        container.removeAllViews();
        container.addView(webView);
        webView.loadUrl(((WXEmbed) comp).src);
      }else{
        super.onException(comp,errCode,msg);
      }
    }
  }

  /**
   * Default event listener.
   */
  public static class ClickToReloadListener implements OnNestedInstanceEventListener {
    @Override
    public void onException(NestedContainer container, String errCode, String msg) {
      if (TextUtils.equals(errCode, WXErrorCode.
              WX_DEGRAD_ERR_NETWORK_BUNDLE_DOWNLOAD_FAILED.getErrorCode()) && container instanceof WXEmbed) {
        final WXEmbed comp = ((WXEmbed)container);
        final ImageView imageView = new ImageView(comp.getContext());
        imageView.setImageResource(R.drawable.weex_error);
        FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ERROR_IMG_WIDTH, ERROR_IMG_HEIGHT);
        layoutParams.gravity = Gravity.CENTER;
        imageView.setLayoutParams(layoutParams);
        imageView.setScaleType(ImageView.ScaleType.FIT_XY);
        imageView.setAdjustViewBounds(true);
        imageView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
            imageView.setOnClickListener(null);
            imageView.setEnabled(false);
            comp.loadContent();
          }
        });
        FrameLayout hostView = comp.getHostView();
        hostView.removeAllViews();
        hostView.addView(imageView);
        WXLogUtils.e("WXEmbed", "NetWork failure :" + errCode + ",\n error message :" + msg);
      }
    }

    @Override
    public boolean onPreCreate(NestedContainer comp, String src) {
      return true;
    }

    @Override
    public String transformUrl(String origin) {
      return origin;
    }

    @Override
    public void onCreated(NestedContainer comp, WXSDKInstance nestedInstance) {

    }
  }

  static class EmbedRenderListener implements IWXRenderListener {
    WXEmbed mComponent;
    OnNestedInstanceEventListener mEventListener;

    EmbedRenderListener(WXEmbed comp) {
      mComponent = comp;
      mEventListener = new ClickToReloadListener();
    }

    @Override
    public void onViewCreated(WXSDKInstance instance, View view) {
      FrameLayout hostView = mComponent.getHostView();
      hostView.removeAllViews();
      hostView.addView(view);
    }

    @Override
    public void onRenderSuccess(WXSDKInstance instance, int width, int height) {

    }

    @Override
    public void onRefreshSuccess(WXSDKInstance instance, int width, int height) {

    }

    @Override
    public void onException(WXSDKInstance instance, String errCode, String msg) {
      if (mEventListener != null) {
        mEventListener.onException(mComponent, errCode, msg);
      }
    }
  }

  @Deprecated
  public WXEmbed(WXSDKInstance instance, WXVContainer parent, String instanceId, boolean isLazy, BasicComponentData basicComponentData) {
    this(instance, parent, basicComponentData);
  }

  public WXEmbed(WXSDKInstance instance, WXVContainer parent, BasicComponentData basicComponentData) {
    super(instance, parent, basicComponentData);
    mListener = new EmbedRenderListener(this);
    mInstanceOnScrollFireEventInterceptor = new EmbedInstanceOnScrollFireEventInterceptor(this);

    ERROR_IMG_WIDTH = (int) WXViewUtils.getRealPxByWidth(270, instance.getInstanceViewPortWidth());
    ERROR_IMG_HEIGHT = (int) WXViewUtils.getRealPxByWidth(260, instance.getInstanceViewPortWidth());
    if (instance instanceof EmbedManager) {
      Object itemId = getAttrs().get(ITEM_ID);
      if (itemId != null) {
        ((EmbedManager) instance).putEmbed(itemId.toString(), this);
      }
    }
    this.priority = WXUtils.getString(getAttrs().get(Constants.Name.PRIORITY), PRIORITY_NORMAL);
    this.strategy = WXUtils.getString(getAttrs().get(Constants.Name.STRATEGY), STRATEGY_NONE);
    instance.getApmForInstance().updateDiffStats(WXInstanceApm.KEY_PAGE_STATS_EMBED_COUNT,1);
  }

  @Override
  public void setOnNestEventListener(OnNestedInstanceEventListener listener){
    mListener.mEventListener = listener;
  }

  @Override
  public ViewGroup getViewContainer() {
    return getHostView();
  }

  @Override
  protected boolean setProperty(String key, Object param) {
    switch (key) {
      case Constants.Name.SRC:
        String src = WXUtils.getString(param, null);
        if (src != null)
          setSrc(src);
        return true;
      case Constants.Name.PRIORITY:
        String priority = WXUtils.getString(param, null);
        if (priority != null) {
          setPriority(priority);
        }
        return true;
    }
    return super.setProperty(key, param);
  }

  @Override
  public void renderNewURL(String url) {
    src = url;
    loadContent();
  }

  @Override
  public void reload() {
    if (!TextUtils.isEmpty(src)) {
      loadContent();
    }
  }

  public String getOriginUrl() {
    return originUrl;
  }

  public void setOriginUrl(String originUrl) {
    this.originUrl = originUrl;
  }

  private String originUrl;


  @Override
  public void addEvent(String type) {
    super.addEvent(type);
    if(Constants.Event.SCROLL_START.equals(type)){
      mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
    }else if(Constants.Event.SCROLL_END.equals(type)){
      mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
    }else if(Constants.Event.SCROLL.equals(type)){
      mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
    }
  }

  @WXComponentProp(name = Constants.Name.SRC)
  public void setSrc(String src) {
    originUrl=src;
    this.src = src;
    if (mNestedInstance != null) {
      mNestedInstance.destroy();
      mNestedInstance = null;
    }
    if (mIsVisible && !TextUtils.isEmpty(this.src)) {
      loadContent();
    }
  }
  public String getSrc() {
    return src;
  }


  @WXComponentProp(name = Constants.Name.PRIORITY)
  public void setPriority(String priority) {
    if(TextUtils.isEmpty(priority)){
      return;
    }
    this.priority = priority;
  }

  /**
   * Load embed content, default behavior is create a nested instance.
   */
  protected void loadContent(){
    if(mNestedInstance != null){
      mNestedInstance.destroy();
    }
    mNestedInstance = createInstance();
    if(mListener != null && mListener.mEventListener != null){
      if(!mListener.mEventListener.onPreCreate(this,src)){
        //cancel render
        mListener.mEventListener.onCreated(this, mNestedInstance);
      }
    }
  }

  private static final int getLevel(WXEmbed embed){
    String priority = embed.priority;
    String strategy = embed.strategy;
    int level  = 5;
    if(!STRATEGY_HIGH.equals(strategy)) {
      if (TextUtils.equals(priority, PRIORITY_LOW)) {
        level = 0;
      } else if (TextUtils.equals(priority, PRIORITY_HIGH)) {
        level = 10;
      }
    }
    return  level;
  }

  private WXSDKInstance createInstance() {
    WXSDKInstance sdkInstance = getInstance().createNestedInstance(this);
    sdkInstance.setParentInstance(getInstance());
    boolean needsAdd = !getAttrs().containsKey("disableInstanceVisibleListener");
    if(needsAdd){ //prevent switch off fire viewappear event twice
        getInstance().addOnInstanceVisibleListener(this);
    }
    sdkInstance.registerRenderListener(mListener);
    mInstanceOnScrollFireEventInterceptor.resetFirstLaterScroller();;
    sdkInstance.addInstanceOnFireEventInterceptor(mInstanceOnScrollFireEventInterceptor);
    sdkInstance.registerOnWXScrollListener(mInstanceOnScrollFireEventInterceptor);

    String url=src;
    if(mListener != null && mListener.mEventListener != null){
      url=mListener.mEventListener.transformUrl(src);
      if(!mListener.mEventListener.onPreCreate(this,src)){
        //cancel render
        return null;
      }
    }

    if(TextUtils.isEmpty(url)){
      mListener.mEventListener.onException(this,
              WXErrorCode.WX_DEGRAD_ERR_BUNDLE_CONTENTTYPE_ERROR.getErrorCode(),
              WXErrorCode.WX_DEGRAD_ERR_BUNDLE_CONTENTTYPE_ERROR.getErrorMsg() + "!!wx embed src url is null"
      );
      return sdkInstance;
    }
    sdkInstance.setContainerInfo(WXInstanceApm.KEY_PAGE_PROPERTIES_INSTANCE_TYPE,"embed");
    sdkInstance.setContainerInfo(WXInstanceApm.KEY_PAGE_PROPERTIES_PARENT_PAGE,getInstance().getWXPerformance().pageName);
    sdkInstance.renderByUrl(url,
            url,
            null, null,
            WXRenderStrategy.APPEND_ASYNC);

    return sdkInstance;
  }

  @Override
  public void setVisibility(String visibility) {
    super.setVisibility(visibility);
    boolean visible = TextUtils.equals(visibility, Constants.Value.VISIBLE);
    if(mIsVisible != visible){

      if (!TextUtils.isEmpty(src) && visible) {
        if (mNestedInstance == null) {
          loadContent();
        } else {
          mNestedInstance.onViewAppear();
        }
      }

      if (!visible) {
        if (mNestedInstance != null) {
          mNestedInstance.onViewDisappear();
        }
      }
      mIsVisible = visible;
      doAutoEmbedMemoryStrategy();
    }
  }

  @Override
  public void destroy() {
    super.destroy();
    destoryNestInstance();
    src = null;
    if (getInstance() != null) {
      getInstance().removeOnInstanceVisibleListener(this);
    }
  }

  private void  doAutoEmbedMemoryStrategy(){
    /**
     * auto manage embed amount in current instance, save memory
     * */
    if(!STRATEGY_NONE.equals(this.strategy)){
      if(!mIsVisible && mNestedInstance != null){
        if(PRIORITY_LOW.equals(this.priority)){
          destoryNestInstance();
        }else{
          if(getInstance().hiddenEmbeds == null){ // low is in front, when priority is same, hidden time pre in first
            getInstance().hiddenEmbeds = new PriorityQueue<>(8, new Comparator<WXEmbed>() {
              @Override
              public int compare(WXEmbed o1, WXEmbed o2) {
                int level =  getLevel(o1) - getLevel(o2);
                if(level != 0){
                  return  level;
                }
                return (int) (o1.hiddenTime - o2.hiddenTime);
              }
            });
          }
          //getInstance().hiddenEmbeds.remove(this);
          if(!getInstance().hiddenEmbeds.contains(this)) {
            this.hiddenTime = System.currentTimeMillis();
            getInstance().hiddenEmbeds.add(this);
          }
          if(getInstance().hiddenEmbeds != null && getInstance().getMaxHiddenEmbedsNum() >= 0){
            while (getInstance().hiddenEmbeds.size() > getInstance().getMaxHiddenEmbedsNum()){
              WXEmbed embed = getInstance().hiddenEmbeds.poll();
              if(embed.mIsVisible){
                continue;
              }
              if(embed != null) {
                embed.destoryNestInstance();
              }
            }
          }
        }
      }
      if(mIsVisible && mNestedInstance != null){
        if(getInstance().hiddenEmbeds != null && getInstance().hiddenEmbeds.contains(this)){
          getInstance().hiddenEmbeds.remove(this);
        }
      }
    }

  }

  @Override
  public void onAppear() {
    //appear event from root instance will not trigger visibility change
    if(mIsVisible && mNestedInstance != null){
      WXComponent comp = mNestedInstance.getRootComponent();
      if(comp != null)
        comp.fireEvent(Constants.Event.VIEWAPPEAR);
    }
  }

  @Override
  public void onDisappear() {
    //appear event from root instance will not trigger visibility change
    if(mIsVisible && mNestedInstance != null){
      WXComponent comp = mNestedInstance.getRootComponent();
      if(comp != null)
        comp.fireEvent(Constants.Event.VIEWDISAPPEAR);
    }
  }

  @Override
  public void onActivityStart() {
    super.onActivityStart();
    if (mNestedInstance != null) {
      mNestedInstance.onActivityStart();
    }
  }

  @Override
  public void onActivityResume() {
    super.onActivityResume();
    if (mNestedInstance != null) {
      mNestedInstance.onActivityResume();
    }
  }

  @Override
  public void onActivityPause() {
    super.onActivityPause();
    if (mNestedInstance != null) {
      mNestedInstance.onActivityPause();
    }
  }

  @Override
  public void onActivityStop() {
    super.onActivityStop();
    if (mNestedInstance != null) {
      mNestedInstance.onActivityStop();
    }
  }

  @Override
  public void onActivityDestroy() {
    super.onActivityDestroy();
    if (mNestedInstance != null) {
      mNestedInstance.onActivityDestroy();
    }
  }

  public void setStrategy(String strategy) {
    this.strategy = strategy;
  }

  private void destoryNestInstance(){
    if(getInstance().hiddenEmbeds != null && getInstance().hiddenEmbeds.contains(this)){
      getInstance().hiddenEmbeds.remove(this);
    }
    if (mNestedInstance != null) {
      mNestedInstance.destroy();
      mNestedInstance = null;
    }
    if(WXEnvironment.isApkDebugable()){
      WXLogUtils.w("WXEmbed destoryNestInstance priority " + priority + " index " + getAttrs().get("index")
              + "  " + hiddenTime  + " embeds size " + (getInstance().hiddenEmbeds == null ?  0 : getInstance().hiddenEmbeds.size())
              + " strategy " + this.strategy);
    }
  }

  @Override
  public void addLayerOverFlowListener(String ref) {
    if (mNestedInstance != null)
      mNestedInstance.addLayerOverFlowListener(getRef());
  }

  @Override
  public void removeLayerOverFlowListener(String ref) {
    if (mNestedInstance != null)
      mNestedInstance.removeLayerOverFlowListener(ref);
  }

  static class EmbedInstanceOnScrollFireEventInterceptor extends
      InstanceOnFireEventInterceptor implements OnWXScrollListener {

    private WXEmbed mEmbed;
    private WXComponent firstLayerScroller;

    public EmbedInstanceOnScrollFireEventInterceptor(WXEmbed embed) {
      this.mEmbed = embed;
    }

    public void resetFirstLaterScroller(){
      firstLayerScroller = null;
    }

    @Override
    public void onFireEvent(String instanceId, String elementRef, String type, Map<String, Object> params, Map<String, Object> domChanges) {
      if(mEmbed == null
              || mEmbed.mNestedInstance == null
              || !mEmbed.mNestedInstance.getInstanceId().equals(instanceId)){
        return;
      }
      if(firstLayerScroller == null){
          initFirstLayerScroller();
      }
      if(firstLayerScroller == null){
        return;
      }
      if(firstLayerScroller.getRef().equals(elementRef)){
         mEmbed.getInstance().fireEvent(mEmbed.getRef(), type, params, domChanges);
      }
    }


    private void initFirstLayerScroller(){
      if(firstLayerScroller == null){
          firstLayerScroller = findFirstLayerScroller();
          if(firstLayerScroller != null){
            for(String event : getListenEvents()){
              if(!firstLayerScroller.containsEvent(event)){
                  firstLayerScroller.getEvents().add(event);
                  firstLayerScroller.addEvent(event);
              }
            }
          }
      }
    }

    /**
     * get first layer scroller ref
     * */
    private WXComponent findFirstLayerScroller(){
         if(mEmbed.mNestedInstance == null){
           return null;
         }
         WXComponent rootComponent = mEmbed.mNestedInstance.getRootComponent();
        if(rootComponent instanceof  Scrollable){
          return rootComponent;
        }
        Queue<WXComponent> queues = new ArrayDeque<>();
        queues.offer(rootComponent);
        while (!queues.isEmpty()){
             WXComponent component = queues.poll();
             if(component == null){
               break;
             }
             if(component instanceof  Scrollable){
                return  component;
             }
             if(component instanceof WXVContainer){
               WXVContainer container = (WXVContainer) component;
               for(int i=0; i<container.getChildCount(); i++){
                  queues.offer(container.getChild(i));
               }
             }
        }
        return null;
    }


    @Override
    public void onScrolled(View view, int x, int y) {
      if(firstLayerScroller != null){
        return;
      }
      if(getListenEvents().size() > 0){
        initFirstLayerScroller();
      }
    }

    @Override
    public void onScrollStateChanged(View view, int x, int y, int newState) {

    }
  }

}
