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

import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build;
import android.text.TextUtils;
import org.apache.weex.IWXStatisticsListener;
import org.apache.weex.WXEnvironment;
import org.apache.weex.adapter.IWXSoLoaderAdapter;
import org.apache.weex.adapter.IWXUserTrackAdapter;
import org.apache.weex.common.WXErrorCode;
import dalvik.system.PathClassLoader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Enumeration;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;


/**
 * Utility class for managing so library, including load native library and version management.
 * <ol>
 *   <li>Load library<br>
 *     It Will try to use {@link System#loadLibrary(String)} to load native library. If it successes,
 *     the Android Framework will be responsible for managing library and library version.
 *     If it fails in case of some ceratin armebi-v7a architecture device, it will try to extract
 *     native library from apk and copy it the data directory of the app. Then load it using
 *     {@link System#load(String)}.
 *     </li>
 *  <li>
 *       Version control for extracting native library from apk.
 *  </li>
 * </ol>
 */
public class WXSoInstallMgrSdk {

  final static String LOGTAG = "INIT_SO";
  //below is the CPU string types
  private final static String ARMEABI = "armeabi"; //default
  private final static String X86 = "x86";
  private final static String MIPS = "mips";
  private final static String STARTUPSO = "/libweexjsb.so";
  private final static String STARTUPSOANDROID15 = "/libweexjst.so";

  static Application mContext = null;
  private static IWXSoLoaderAdapter mSoLoader = null;
  private static IWXStatisticsListener mStatisticsListener = null;

  private static String mAbi = null;

  public static void init(Application c,
                          IWXSoLoaderAdapter loader,
                          IWXStatisticsListener listener) {
    mContext = c;
    mSoLoader = loader;
    mStatisticsListener = listener;
  }

  public static boolean isX86(){
    String cpuType = _cpuType();
    return cpuType.equalsIgnoreCase(X86);
  }

  public static boolean isCPUSupport(){
    String cpuType = _cpuType();
    return !cpuType.equalsIgnoreCase(MIPS);
  }

  /**
   * Load so library.
   *
   * If a library loader adapter exists, use this adapter to load library,
   * otherwise use {@link System#loadLibrary(String)} to load library.
   * If failed to load library, try to extract the so library and load it
   * from arembi in the .apk
   *
   * @param libName library name, like webp, not necessary to be libwep.so
   * @param version the version of the so library
   */
  public static boolean initSo(String libName, int version, IWXUserTrackAdapter utAdapter) {
    String cpuType = _cpuType();
    if (cpuType.equalsIgnoreCase(MIPS) ) {
      WXExceptionUtils.commitCriticalExceptionRT(null,
              WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
              "initSo", "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for android cpuType is MIPS",
              null);
      return false;
    }

    // copy startup so
    if (WXEnvironment.CORE_SO_NAME.equals(libName)) {
      copyStartUpSo();
    }


    boolean InitSuc = false;
      try {
        // If a library loader adapter exists, use this adapter to load library
        // instead of System.loadLibrary.
        if (mSoLoader != null) {
          mSoLoader.doLoadLibrary("c++_shared");
        } else {
          System.loadLibrary("c++_shared");
        }
      } catch (Exception | Error e) {
        WXExceptionUtils.commitCriticalExceptionRT(null,
                WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                "initSo",
                        "load c++_shared failed Detail Error is: " +e.getMessage(),
                null);

        if(WXEnvironment.isApkDebugable()) {
          throw e;
        }
      }

      /**
       * Load library with {@link System#loadLibrary(String)}
       */
      try {
        // If a library loader adapter exists, use this adapter to load library
        // instead of System.loadLibrary.
        if (mSoLoader != null) {
          mSoLoader.doLoadLibrary(libName);
        } else {
          System.loadLibrary(libName);
        }

        InitSuc = true;
      } catch (Exception | Error e2) {
        if (cpuType.contains(ARMEABI) || cpuType.contains(X86)) {
          WXExceptionUtils.commitCriticalExceptionRT(null,
                  WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
                  "initSo", libName + "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for android cpuType is " +cpuType +
                          "\n Detail Error is: " +e2.getMessage(),
                  null);
        }

        if(WXEnvironment.isApkDebugable()) {
          throw e2;
        }
        InitSuc = false;
      }

//      try {

//        if (!InitSuc) {
//
//          //File extracted from apk already exists.
//          if (isExist(libName, version)) {
//            boolean res = _loadUnzipSo(libName, version, utAdapter);
//            if (res) {
//              return res;
//            } else {
//              //Delete the corrupt so library, and extract it again.
//              removeSoIfExit(libName, version);
//            }
//          }
//
//          //Fail for loading file from libs, extract so library from so and load it.
//          if (cpuType.equalsIgnoreCase(MIPS)) {
//            return false;
//          } else {
//            try {
//              InitSuc = unZipSelectedFiles(libName, version, utAdapter);
//            } catch (IOException e2) {
//              e2.printStackTrace();
//            }
//          }
//
//        }
//      } catch (Exception | Error e) {
//        InitSuc = false;
//        e.printStackTrace();
//      }
  //  }
    return InitSuc;
  }

  private static File _desSoCopyFile(String soName) {
    String cpuType = _cpuType();
    String copyPath = WXEnvironment.copySoDesDir();
    if (TextUtils.isEmpty(copyPath)) {
      return null;
    }
    File desDir = new File(copyPath, soName + "/" + cpuType);
    return desDir;
  }

  /**
   * copyStartUpSo
   */
  @SuppressLint("SdCardPath")
  public static void copyStartUpSo() {
    try {
      // copy libjsb.so to cache/weex/jsb/cputype
      String pkgName = WXEnvironment.getApplication().getPackageName();
      String cacheFile = WXEnvironment.getApplication().getApplicationContext().getCacheDir().getPath();

      // cp weexjsb any way
      // if android api < 16 copy libweexjst.so else copy libweexjsb.so
      boolean pieSupport = true;
      File newfile;
      String startSoName = WXEnvironment.CORE_JSB_SO_NAME;
      String startSoPath = STARTUPSO;
      if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        pieSupport = false;
        startSoName = WXEnvironment.CORE_JST_SO_NAME;
        startSoPath = STARTUPSOANDROID15;
      }

      final File copyPath = _desSoCopyFile(startSoName);
      if(!copyPath.exists()) {
        copyPath.mkdirs();
      }
      newfile = new File(copyPath, startSoPath);
      WXEnvironment.CORE_JSB_SO_PATH = newfile.getAbsolutePath();
      String jsb = WXEnvironment.getDefaultSettingValue(startSoName, "-1");
      if(newfile.exists() && TextUtils.equals(WXEnvironment.getAppVersionName(), jsb)) {
        // no update so skip copy
        return;
      }

      String path = "/data/data/" + pkgName + "/lib";
      if (cacheFile != null && cacheFile.indexOf("/cache") > 0) {
        path = cacheFile.replace("/cache", "/lib");
      }
      File oldfile = null;
      if (pieSupport) {
        oldfile = new File(path, STARTUPSO);
      } else {
        oldfile = new File(path , STARTUPSOANDROID15);
      }


      if (!oldfile.exists()) {
        try {
          String weexjsb = ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary(startSoName);
          oldfile = new File(weexjsb);
        } catch (Throwable throwable) {
          // do nothing
        }
      }


      if(!oldfile.exists()) {
        WXEnvironment.extractSo();
        oldfile = new File(copyPath, STARTUPSO);
      }

      if (oldfile.exists()) {
        WXFileUtils.copyFile(oldfile, newfile);
      }
      WXEnvironment.writeDefaultSettingsValue(startSoName, WXEnvironment.getAppVersionName());
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }

  public static void copyJssRuntimeSo(){
    boolean tryUseRunTimeApi = WXUtils.checkGreyConfig("wxapm","use_runtime_api","0");
    WXLogUtils.e("weex", "tryUseRunTimeApi ? "+ tryUseRunTimeApi);
    if (!tryUseRunTimeApi){
      return;
    }
    try {
      WXLogUtils.e("weex", "copyJssRuntimeSo: ");
      File toPath = _desSoCopyFile(WXEnvironment.CORE_JSS_SO_NAME);
      if(!toPath.exists()) {
        toPath.mkdirs();
      }
      File targetFile = new File(toPath,"libweexjss.so");

      /** 1. check so and versionCode. if update, then rm old jss.so(runtime) in pkg/libs, and copy new so from apk **/
      String keyVersionCode = "app_version_code_weex";
      String defaultSettingValue = WXEnvironment.getDefaultSettingValue(keyVersionCode, "-1");


      if (targetFile.exists()){
        if (!TextUtils.equals(WXEnvironment.getAppVersionName(),defaultSettingValue)){
          targetFile.delete();
        }else {
          WXEnvironment.CORE_JSS_SO_PATH= targetFile.getAbsolutePath();
          WXEnvironment.sUseRunTimeApi = true;
          WXLogUtils.e("weex", "copyJssRuntimeSo exist:  return");
          return;
        }
      }
      /** 2. copy jss(runtime) so **/
      String fromPath =  ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary("weexjssr");
      if (TextUtils.isEmpty(fromPath)){
        return;
      }
      targetFile.createNewFile();
      WXFileUtils.copyFileWithException(new File(fromPath),targetFile);
      /**3. update flag **/
      WXEnvironment.CORE_JSS_SO_PATH= targetFile.getAbsolutePath();
      WXEnvironment.writeDefaultSettingsValue(keyVersionCode,WXEnvironment.getAppVersionName());
      WXEnvironment.sUseRunTimeApi = true;
      WXLogUtils.e("weex", "copyJssRuntimeSo: cp end and return ");
    }catch (Throwable e){
      e.printStackTrace();
      WXEnvironment.sUseRunTimeApi = false;
      WXLogUtils.e("weex", "copyJssRuntimeSo:  exception" + e);
    }
  }

  private static String _getFieldReflectively(Build build, String fieldName) {
    try {
      final Field field = Build.class.getField(fieldName);
      return field.get(build).toString();
    } catch (Exception ex) {
      return "Unknown";
    }
  }

  private static String _cpuType() {
    if(TextUtils.isEmpty(mAbi)) {
      try {
        mAbi = Build.CPU_ABI;
      }catch (Throwable e){
        e.printStackTrace();
        mAbi = ARMEABI;
      }
      if (TextUtils.isEmpty(mAbi)){
        mAbi = ARMEABI;
      }
      mAbi = mAbi.toLowerCase(Locale.ROOT);
    }
    return mAbi;
  }

  /**
   *
   * @param libName lib name
   * @param size  the right size of lib
   * @return true for valid  ; false for InValid
   */
  static boolean checkSoIsValid(String libName, long size) {
    Context context = mContext;
    if (null == context) {
      return false;
    }
    try{
      long start=System.currentTimeMillis();
      if(WXSoInstallMgrSdk.class.getClassLoader() instanceof PathClassLoader ) {

        String path = ((PathClassLoader) (WXSoInstallMgrSdk.class.getClassLoader())).findLibrary(libName);
        if(TextUtils.isEmpty(path) ){
          return false;
        }
        File file = new File(path);

        if (!file.exists() || size == file.length()) {
          WXLogUtils.w("weex so size check path :" + path+"   "+(System.currentTimeMillis() - start));
          return true;
        } else {
          return false;
        }
      }
    }catch(Throwable e ){
      WXExceptionUtils.commitCriticalExceptionRT(null,
              WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT,
              "checkSoIsValid", "[WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT] for " +
                      "weex so size check fail exception :"+e.getMessage(),
              null);
      WXLogUtils.e("weex so size check fail exception :"+e.getMessage());
    }

    return true;
  }

  /**
   * Concatenate the path of the so library, including directory.
   * @param libName the raw name of the lib
   * @param version the version of the so library
   * @return the path of the so library
   */
  @SuppressLint("SdCardPath")
  static String _targetSoFile(String libName, int version) {
    Context context = mContext;
    if (null == context) {
      return "";
    }

    String path = "/data/data/" + context.getPackageName() + "/files";

    File f = context.getFilesDir();
    if (f != null) {
      path = f.getPath();
    }
    return path + "/lib" + libName + "bk" + version + ".so";

  }

  /**
   * Remove the so library if it had been extracted.
   * @param libName
   * @param version
   */
  static void removeSoIfExit(String libName, int version) {

    String file = _targetSoFile(libName, version);
    File a = new File(file);
    if (a.exists()) {
      a.delete();
    }

  }

  /**
   * Tell whether the so is extracted.
   */
  static boolean isExist(String libName, int version) {

    String file = _targetSoFile(libName, version);
    File a = new File(file);
    return a.exists();

  }


  /**
   * Load .so library
   */
  @SuppressLint("UnsafeDynamicallyLoadedCode")
  static boolean _loadUnzipSo(String libName,
                              int version,
                              IWXUserTrackAdapter utAdapter) {
    boolean initSuc = false;
    try {
      if (isExist(libName, version)) {
        // If a library loader adapter exists, use this adapter to load library
        // instead of System.load.
        if (mSoLoader != null) {
          mSoLoader.doLoad(_targetSoFile(libName, version));
        } else {
          System.load(_targetSoFile(libName, version));
        }
      }
      initSuc = true;
    } catch (Throwable e) {
      initSuc = false;
      WXExceptionUtils.commitCriticalExceptionRT(null,
              WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT,
              "_loadUnzipSo", "[WX_KEY_EXCEPTION_SDK_INIT_WX_ERR_COPY_FROM_APK] " +
                      "\n Detail Msg is : " +  e.getMessage(),
              null);
      WXLogUtils.e("", e);
    }
    return initSuc;
  }

  static boolean unZipSelectedFiles(String libName,
                                    int version,
                                    IWXUserTrackAdapter utAdapter) throws ZipException, IOException {
    String sourcePath = "lib/armeabi/lib" + libName + ".so";

    String zipPath = "";
    Context context = mContext;
    if (context == null) {
      return false;
    }

    ApplicationInfo aInfo = context.getApplicationInfo();
    if (null != aInfo) {
      zipPath = aInfo.sourceDir;
    }

    ZipFile zf;
    zf = new ZipFile(zipPath);
    try {

      for (Enumeration<?> entries = zf.entries(); entries.hasMoreElements(); ) {
        ZipEntry entry = ((ZipEntry) entries.nextElement());
        if (entry.getName().startsWith(sourcePath)) {

          InputStream in = null;
          FileOutputStream os = null;
          FileChannel channel = null;
          int total = 0;
          try {

            //Make sure the old library is deleted.
            removeSoIfExit(libName, version);

            //Copy file
            in = zf.getInputStream(entry);
            os = context.openFileOutput("lib" + libName + "bk" + version + ".so",
                    Context.MODE_PRIVATE);
            channel = os.getChannel();

            byte[] buffers = new byte[1024];
            int realLength;

            while ((realLength = in.read(buffers)) > 0) {
              //os.write(buffers);
              channel.write(ByteBuffer.wrap(buffers, 0, realLength));
              total += realLength;

            }
          } finally {
            if (in != null) {
              try {
                in.close();
              } catch (Exception e) {
                e.printStackTrace();
              }
            }

            if (channel != null) {
              try {
                channel.close();
              } catch (Exception e) {
                e.printStackTrace();
              }
            }

            if (os != null) {
              try {
                os.close();
              } catch (Exception e) {
                e.printStackTrace();
              }
            }

            if (zf != null) {
              zf.close();
              zf = null;
            }
          }

          if (total > 0) {
            return _loadUnzipSo(libName, version, utAdapter);
          } else {
            return false;
          }
        }
      }
    } catch (java.io.IOException e) {
      e.printStackTrace();
      WXExceptionUtils.commitCriticalExceptionRT(null,
              WXErrorCode.WX_KEY_EXCEPTION_SDK_INIT_CPU_NOT_SUPPORT,
              "unZipSelectedFiles", "[WX_KEY_EXCEPTION_SDK_INIT_unZipSelectedFiles] " +
                      "\n Detail msg is: " + e.getMessage(),
              null);

    } finally {

      if (zf != null) {
        zf.close();
        zf = null;
      }
    }
    return false;
  }

  /**
   * Using {@Code WXExceptionUtils.commitCriticalExceptionRT}  insted
   */
//  static void commit(IWXUserTrackAdapter utAdapter, String errCode, String errMsg) {
//    if (mStatisticsListener != null) {
//      mStatisticsListener.onException("0", errCode, errMsg);
//    }
//
//    if (utAdapter == null) {
//      return;
//    }
//    if (errCode != null && errMsg != null) {
//      WXPerformance p = new WXPerformance();
//      p.errCode = errCode;
//      p.errMsg = errMsg;
//      utAdapter.commit(null, null, WXEnvironment.ENVIRONMENT, p, null);
//    } else {
//      utAdapter.commit(null, null, WXEnvironment.ENVIRONMENT, null, null);
//
//    }
//  }

}
