/*
 * Copyright 2013 ThinkFree
 *
 * 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 org.ruboto;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.os.Build;
import android.util.Log;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Observable;
import java.util.Observer;

import dalvik.system.PathClassLoader;

/**
 * Easy class loading for multi-dex Android application.
 *
 * 1) call validateClassPath() from Application.onCreate()
 * 2) check dexOptRequired then addAllJARsAssets() on non-UI thread.
 *
 * @author Alan Goo
 */
public class DexDex {
    public static final String DIR_SUBDEX = "dexdex";

    private static final String TAG = "DexDex";

    private static final int SDK_INT_ICS = 14;
    private static final int SDK_INT_KITKAT = 19;
    private static final int SDK_INT_MARSHMALLOW = 23;

    private static final int BUF_SIZE = 8 * 1024;
    public static final int PROGRESS_COMPLETE = 100;

    private static ArrayList<String> theAppended = new ArrayList<String>();

    public static boolean debug = false;

    public static boolean dexOptRequired = false;

    private static Activity uiBlockedActivity = null;

    /**
     * just reuse existing interface for convenience
     * @hide
     */
    public static Observer dexOptProgressObserver = null;

    private DexDex() {
        // do not create an instance
    }

    private static boolean shouldDexOpt(File apkFile, File dexDir, String[] names) {
        boolean result = shouldDexOptImpl(apkFile, dexDir, names);
        if(debug) {
            Log.d(TAG, "shouldDexOpt(" + apkFile + "," + dexDir + "," + Arrays.deepToString(names) + ") => " + result
                + " on " + Thread.currentThread());
        }
        return result;
    }

    private static boolean shouldDexOptImpl(File apkFile, File dexDir, String[] names) {
        long apkDate = apkFile.lastModified();
        // APK upgrade case
        if(debug) {
            Log.d(TAG, "APK Date : " + apkDate + " ,dexDir date : " + dexDir.lastModified());
        }
        if (apkDate > dexDir.lastModified()) {
            return true;
        }
        // clean install (or crash during install) case
        for (int i = 0; i < names.length; i++) {
            String name = names[i];
            File dexJar = new File(dexDir, name);
            if (dexJar.exists()) {
                if (dexJar.lastModified() < apkDate) {
                    return true;
                }
            } else {
                return true;
            }
        }
        return false;
    }

    /**
     * Should be called from <code>Application.onCreate()</code>.
     * it returns quickly with little disk I/O.
     */
    public static void validateClassPath(final Context app) {
        try {
            String[] arrJars = createSubDexList(app);
            if(debug) {
                Log.d(TAG, "validateClassPath : " + Arrays.deepToString(arrJars));
            }
            File apkFile = new File(app.getApplicationInfo().sourceDir);
            final File dexDir = app.getDir(DIR_SUBDEX, Context.MODE_PRIVATE); // this API creates the directory if not exist
            dexOptRequired = shouldDexOpt(apkFile, dexDir, arrJars);
            if (dexOptRequired) {
                Thread dexOptThread = new Thread("DexDex - DexOpting for " + Arrays.deepToString(arrJars)) {
                    @Override
                    public void run() {
                        DexDex.addAllJARsInAssets(app);
                        // finished
                        dexOptRequired = false;

                        if(dexOptProgressObserver!=null) {
                            dexOptProgressObserver.update(null, PROGRESS_COMPLETE);
                            dexOptProgressObserver = null;
                        }

                        if (uiBlockedActivity != null) {
                            uiBlockedActivity.runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    // FIXME(uwe): Simplify when we stop supporting android-11
                                    // if (Build.VERSION.SDK_INT < 11) {
                                        Intent callerIntent = uiBlockedActivity.getIntent();
                                        uiBlockedActivity.finish();
                                        uiBlockedActivity.startActivity(callerIntent);
                                    // } else {
                                    //     uiBlockedActivity.recreate();
                                    // }
                                    // EMXIF
                                    uiBlockedActivity = null;
                                }
                            });
                        }
                    }
                };
                dexOptThread.start();
            } else {
                // all dex JAR are stable
                appendOdexesToClassPath(app, dexDir, arrJars);
            }
            if(debug) {
                Log.d(TAG, "validateClassPath - dexDir : " + dexDir);
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /** find and append all JARs */
    public static void addAllJARsInAssets(final Context cxt) {
        try {
            if(debug) {
                Log.d(TAG, "addAllJARsInAssets on " + Thread.currentThread());
            }
            String[] arrJars = createSubDexList(cxt);
            copyJarsFromAssets(cxt, arrJars);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private static String[] createSubDexList(final Context cxt) throws IOException {
        String[] files = cxt.getAssets().list("");
        ArrayList<String> jarList = new ArrayList<String>();
        for (int i = 0; i < files.length; i++) {
            String jar = files[i];
            if (jar.endsWith(".jar")) {
                jarList.add(jar);
            }
        }
        String[] arrJars = new String[jarList.size()];
        jarList.toArray(arrJars);
        return arrJars;
    }

    /**
     * MUST be called on non-Main Thread
     * @param names array of file names in 'assets' directory
     */
    public static void copyJarsFromAssets(final Context cxt, final String[] names) {
        if(debug) {
            Log.d(TAG, "copyJarsFromAssets(" + Arrays.deepToString(names) + ")");
        }
        final File dexDir = cxt.getDir(DIR_SUBDEX, Context.MODE_PRIVATE); // this API creates the directory if not exist
        File apkFile = new File(cxt.getApplicationInfo().sourceDir);
        // should copy subdex JARs to dexDir?
        final boolean shouldInit = shouldDexOpt(apkFile, dexDir, names);
        if (shouldInit) {
            try {
                copyToInternal(cxt, dexDir, names);
                appendOdexesToClassPath(cxt, dexDir, names);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
        } else {
            if (!inAppended(names)) {
                appendOdexesToClassPath(cxt, dexDir, names);
            }
        }
    }

    /** checks if all <code>names</code> elements are in <code>theAppended</code> */
    private static boolean inAppended(String[] names) {
        for (int i = 0; i < names.length; i++) {
            if (!theAppended.contains(names[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * append DexOptimized dex files to the classpath.
     * @return true if additional DexOpt is required, false otherwise.
     */
    private static boolean appendOdexesToClassPath(Context cxt, File dexDir, String[] names) {
        // non-existing ZIP in classpath causes an exception on ICS
        // so filter out the non-existent
        String strDexDir = dexDir.getAbsolutePath();
        ArrayList<String> jarPaths = new ArrayList<String>();
        for (int i = 0; i < names.length; i++) {
            String jarPath = strDexDir + '/' + names[i];
            File f = new File(jarPath);
            if (f.isFile()) {
                jarPaths.add(jarPath);
            }
        }

        String[] jarsOfDex = new String[jarPaths.size()];
        jarPaths.toArray(jarsOfDex);

        PathClassLoader pcl = (PathClassLoader) cxt.getClassLoader();
        // do something dangerous
        try {
            if (Build.VERSION.SDK_INT < SDK_INT_ICS) {
                FrameworkHack.appendDexListImplUnderICS(jarsOfDex, pcl, dexDir);
            } else { // ICS+
                boolean kitkatPlus = Build.VERSION.SDK_INT >= SDK_INT_KITKAT;
                boolean marshmallowPlus = Build.VERSION.SDK_INT >= SDK_INT_MARSHMALLOW;
                ArrayList<File> jarFiles = DexDex.strings2Files(jarsOfDex);
                FrameworkHack.appendDexListImplICS(jarFiles, pcl, dexDir, kitkatPlus, marshmallowPlus);
            }
            // update theAppended if succeeded to prevent duplicated classpath entry
            for (String jarName : names) {
                theAppended.add(jarName);
            }
            if(debug) {
                Log.d(TAG, "appendOdexesToClassPath completed : " + pcl);
                Log.d(TAG, "theAppended : " + theAppended);
            }
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        return true;
    }

    private static void copyToInternal(Context cxt, File destDir, String[] names) {
        String strDestDir = destDir.getAbsolutePath();
        AssetManager assets = cxt.getAssets();
        byte[] buf = new byte[BUF_SIZE];
        for (int i = 0; i < names.length; i++) {
            String name = names[i];
            String destPath = strDestDir + '/' + name;

            try {
                BufferedInputStream bis = new BufferedInputStream(assets.open(name));
                BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath));
                int len;
                while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                    bos.write(buf, 0, len);
                }
                bis.close();
                bos.close();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
        destDir.setLastModified(System.currentTimeMillis());
    }

    private static ArrayList<File> strings2Files(String[] paths) {
        ArrayList<File> result = new ArrayList<File>(paths.length);
        int size = paths.length;
        for (int i = 0; i < size; i++) {
            result.add(new File(paths[i]));
        }
        return result;
    }

    public static void showUiBlocker(Activity startActivity, CharSequence title, CharSequence msg) {
        if(debug) {
            Log.d(TAG, "showUiBlocker() for " + startActivity);
        }
        uiBlockedActivity = startActivity;
        final ProgressDialog progressDialog = new ProgressDialog(startActivity);
        progressDialog.setMessage(msg);
        progressDialog.setTitle(title);
        progressDialog.setIndeterminate(true);
        dexOptProgressObserver = new Observer() {
            @Override
            public void update(Observable observable, Object o) {
                if(o==Integer.valueOf(PROGRESS_COMPLETE)) {
                    progressDialog.dismiss();
                }
            }
        };
        
        progressDialog.show();
    }
}