package de.mj.cordova.plugin.filelogger;

import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.Toast;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class LogCatPlugin extends CordovaPlugin {

    public final static String TAG = "LogCatPlugin";

    private final static String EXTERNAL_STORAGE_FOLDER = TAG;
    private final static String DEFAULT_LC_FILENAME = TAG + "Log.txt";
    private final static String DEFAULT_JS_FILENAME = TAG + "JsLog.txt";
    private final static String DEFAULT_ZIP_FILENAME = TAG + ".zip";
    private final static String ARRAY_SEPARATOR = "--entry--";
    private final static String LOGCAT_COMMAND = "logcat -v long";
    private final static String LOGCAT_CLEAR_BUFFER_COMMAND = "logcat -c";
    private final static String LOG_ROLLING_EXTENSION = "bak";
    private final static String LOG_CON_SUFFIX = "_con";

    private final static int DEFAULT_MAX_FILESIZE_IN_KB = 1024;
    private final static int DEFAULT_MAX_ENTRIES_TO_OUTPUT = 10;
    private final static int PERMISSION_REQUEST_CODE = 1;

    private CordovaInterface cordovaInstance = null;
    private CordovaWebView webView = null;
    private Context context = null;

    private int maxFileSizeInKB;
    private boolean enableCallback;

    private BashExecutor bashExecuter;
    private JsFileWriter jsFileWriter = null;
    private Thread loggerThread = null;

    private File jsFile;
    private File jsBak;
    private File jsCon;

    private File lcFile;
    private File lcBak;
    private File lcCon;

    private File zipFile;

    private File externalStorage;
    private File internalStorage;

    private String[] filterBy;
    private String[] filterOut;


    public enum ACTION {
        INIT_LOGGER
                ("init"),
        START_LOGGER
                ("startLogger"),
        STOP_LOGGER
                ("stopLogger"),
        JS_LOG
                ("jsLog"),
        DELETE_LOG
                ("deleteLog"),
        GET_JS_LOG_PATH
                ("getJsLogPath"),
        GET_LC_LOG_PATH
                ("getLcLogPath"),
        GET_LAST_LC_ENTRIES
                ("getLastLcEntries"),
        GET_LAST_JS_ENTRIES
                ("getLastJsEntries"),
        ZIP_ALL
                ("zipAll"),
        SHOW_IN_FILE_MANAGER
                ("showInFileManager"),
        CLEAR_LC_BUFFER
                ("clearLcBuffer"),
        THROW_EXAMPLE_ERROR
                ("throwExampleError"),
        THROW_EXAMPLE_FATAL_ERROR
                ("throwExampleFatalError");

        private final String action;

        ACTION(final String action) {
            this.action = action;
        }

        @Override
        public String toString() {
            return action;
        }
    }

    private enum RETURN_CODE {
        NO_LOG_FILES_FOUND,
        CANT_CREATE_ZIP,
        CANT_OPEN_LOG,
        PLUGIN_NOT_INITIALIZED,
        NO_ENTRIES_FOUND,
        NO_EXTERNAL_STORAGE_PERMISSIONS,
        COULD_NOT_DELETE_FILE,
        NO_FILE_MANAGER_FOUND
    }

    @Override
    public void initialize(final CordovaInterface cordova, final CordovaWebView webView) {
        Log.v(TAG, "Init");
        this.webView = super.webView;
        this.cordovaInstance = super.cordova;
        this.context = cordovaInstance.getActivity().getApplicationContext();
        this.internalStorage = this.context.getFilesDir();
        this.externalStorage = new File(Environment.getExternalStorageDirectory() +
                File.separator + EXTERNAL_STORAGE_FOLDER);
    }

    @Override
    public boolean execute(final String action, final JSONArray args,
                           final CallbackContext callbackContext) throws JSONException {

        if (action.equals(ACTION.INIT_LOGGER.toString())) {
            this.initLogger(args, callbackContext);
        } else if (action.equals(ACTION.START_LOGGER.toString())) {
            this.startLogging(callbackContext);
        } else if (action.equals(ACTION.SHOW_IN_FILE_MANAGER.toString())) {
            this.showInFileManager(callbackContext);
        } else if (action.equals(ACTION.DELETE_LOG.toString())) {
            this.deleteLogFiles(callbackContext);
        } else if (action.equals(ACTION.THROW_EXAMPLE_ERROR.toString())) {
            this.throwExampleError(callbackContext);
        } else if (action.equals(ACTION.THROW_EXAMPLE_FATAL_ERROR.toString())) {
            this.throwExampleFatalError();
        } else if (action.equals(ACTION.STOP_LOGGER.toString())) {
            this.stopLogging(callbackContext);
        } else if (action.equals(ACTION.ZIP_ALL.toString())) {
            this.zipAll(callbackContext);
        } else if (action.equals(ACTION.CLEAR_LC_BUFFER.toString())) {
            this.clearLogCatBuffer(callbackContext);
        } else if (action.equals(ACTION.GET_LC_LOG_PATH.toString())) {
            this.getJcLogPath(callbackContext);
        } else if (action.equals(ACTION.GET_JS_LOG_PATH.toString())) {
            this.getJsLogPath(callbackContext);
        } else if (action.equals(ACTION.JS_LOG.toString())) {
            this.writeToJsLog(args);
        } else if (action.equals(ACTION.GET_LAST_LC_ENTRIES.toString())) {
            this.getLastLcEntries(args, callbackContext);
        } else if (action.equals(ACTION.GET_LAST_JS_ENTRIES.toString())) {
            this.getLastJsEntries(args, callbackContext);
        } else {
            return false;
        }

        return true;
    }

    private void getJcLogPath(final CallbackContext callbackContext) {
        final File fileToCopy = FileTools.prepareDownload(lcFile, lcBak, lcCon);
        if (fileToCopy != null && fileToCopy.exists()) {
            callbackContext.success(fileToCopy.getAbsolutePath());
        } else {
            callbackContext.error(RETURN_CODE.NO_LOG_FILES_FOUND.name());
        }
    }

    private void getJsLogPath(final CallbackContext callbackContext) {
        final File fileToCopy = FileTools.prepareDownload(jsFile, jsBak, jsCon);
        if (fileToCopy != null && fileToCopy.exists()) {
            callbackContext.success(fileToCopy.getAbsolutePath());
        } else {
            callbackContext.error(RETURN_CODE.NO_LOG_FILES_FOUND.name());
        }
    }

    private void getLastLcEntries(final JSONArray args, final CallbackContext callbackContext) {
        getLogEntries(lcFile, lcBak, lcCon, args, callbackContext);
    }

    private void getLastJsEntries(final JSONArray args, final CallbackContext callbackContext) {
        getLogEntries(jsFile, jsBak, jsCon, args, callbackContext);
    }

    private void getLogEntries(final File log, final File logBak, final File logCat,
                               final JSONArray args, final CallbackContext callbackContext) {
        cordovaInstance.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                final String[] filterBy = args.optString(0) == null ?
                        null : args.optString(0).split(ARRAY_SEPARATOR);
                final String[] filterOut = args.optString(1) == null ?
                        null : args.optString(1).split(ARRAY_SEPARATOR);
                int maxEntries = args.optString(2) != null ?
                        args.optInt(2) : DEFAULT_MAX_ENTRIES_TO_OUTPUT;
                final LogFileReader reader = new LogFileReader(log, logBak,
                        logCat, maxEntries, filterBy, filterOut);

                final List<LogEntry> entries;
                try {
                    entries = reader.getLatestEntries();
                    if (entries == null || entries.isEmpty()) {
                        callbackContext.error(RETURN_CODE.NO_ENTRIES_FOUND.name());
                    } else {
                        String output = "";
                        for (final LogEntry entry : entries) {
                            output += entry.toString() + "\n\n";
                        }
                        callbackContext.success(output);
                    }
                } catch (IOException e) {
                    callbackContext.error(RETURN_CODE.CANT_OPEN_LOG.name());
                    Log.v(LogCatPlugin.TAG, Log.getStackTraceString(e));
                }
            }
        });
    }

    private void clearLogCatBuffer(final CallbackContext callbackContext) {
        final BashExecutor executor = new BashExecutor();
        executor.setCommand(LOGCAT_CLEAR_BUFFER_COMMAND);
        cordovaInstance.getThreadPool().execute(executor);
        callbackContext.success();
    }

    private void throwExampleError(final CallbackContext callbackContext) {
        cordovaInstance.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                    Log.v(LogCatPlugin.TAG,
                            "Should appear after the NullPointerException");
                    callbackContext.success();
                } catch (InterruptedException e) {
                    //
                }
            }
        });
        ((String) null).length();
    }

    private void throwExampleFatalError() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(context, null, Toast.LENGTH_SHORT).show();
            }
        }).start();
    }

    private void deleteLogFiles(final CallbackContext callbackContext) {
        try {
            if (this.lcFile.exists()) {
                this.lcFile.delete();
            }

            if (this.lcBak.exists()) {
                this.lcBak.delete();
            }

            if (this.lcCon.exists()) {
                this.lcCon.delete();
            }

            if (this.jsFile.exists()) {
                this.jsFile.delete();
            }

            if (this.jsBak.exists()) {
                this.jsBak.delete();
            }

            if (this.jsCon.exists()) {
                this.jsCon.delete();
            }

            callbackContext.success();
        } catch (Exception e) {
            callbackContext.error(RETURN_CODE.COULD_NOT_DELETE_FILE.name());
        }
    }

    private void zipAll(final CallbackContext callbackContext) {
        cordovaInstance.getThreadPool().execute(new Runnable() {
            @Override
            public void run() {
                if (zipFile == null) {
                    callbackContext.error(RETURN_CODE.PLUGIN_NOT_INITIALIZED.name());
                }

                final File logCatToZip = FileTools.prepareDownload(lcFile, lcBak, lcCon);
                final File jsFileToZip = FileTools.prepareDownload(jsFile, jsBak, jsCon);

                if (zipFile != null && zipFile.exists()) {
                    zipFile.delete();
                }

                final ArrayList<File> filesToZip = new ArrayList<File>();

                if (logCatToZip != null && logCatToZip.exists()) {
                    filesToZip.add(logCatToZip);
                }

                if (jsFileToZip != null && jsFileToZip.exists()) {
                    filesToZip.add(jsFileToZip);
                }

                if (!filesToZip.isEmpty()) {
                    final File[] fileList = filesToZip.toArray(new File[filesToZip.size()]);
                    final Runnable zipThread = new Runnable() {
                        @Override
                        public void run() {
                            final File zippedLog = Zipper.zipLog(fileList, zipFile);
                            if (zippedLog != null) {
                                callbackContext.success(zippedLog.getAbsolutePath());
                            } else {
                                callbackContext.error(RETURN_CODE.CANT_CREATE_ZIP.name());
                            }
                        }
                    };
                    cordovaInstance.getThreadPool().execute(zipThread);
                } else {
                    callbackContext.error(RETURN_CODE.NO_LOG_FILES_FOUND.name());
                }
            }
        });
    }

    private void showInFileManager(final CallbackContext callbackContext) {
        if (this.checkStoragePermission()) {
            final Runnable showFileManager = new Runnable() {
                @Override
                public void run() {
                    prepareFilesToShow(callbackContext);
                    callbackContext.success();
                }
            };
            cordovaInstance.getThreadPool().execute(showFileManager);
        } else {
            callbackContext.error(RETURN_CODE.NO_EXTERNAL_STORAGE_PERMISSIONS.name());
        }
    }

    private void prepareFilesToShow(final CallbackContext callbackContext) {
        if (!this.externalStorage.exists()) {
            this.externalStorage.mkdir();
        }

        if (this.externalStorage.exists()) {
            File targetFile = null;
            File jsTargetFile = null;

            final File fileToCopy = FileTools.prepareDownload(lcFile, lcBak, lcCon);
            if (fileToCopy != null) {
                targetFile = new File(externalStorage,
                        fileToCopy.getName().replace(LOG_CON_SUFFIX, ""));
            }

            final File jsFileToCopy = FileTools.prepareDownload(jsFile, jsBak, jsCon);
            if (jsFileToCopy != null) {
                jsTargetFile = new File(externalStorage,
                        jsFileToCopy.getName().replace(LOG_CON_SUFFIX, ""));
            }

            if (targetFile != null && targetFile.exists()) {
                targetFile.delete();
            }

            if (jsTargetFile != null && jsTargetFile.exists()) {
                jsTargetFile.delete();
            }

            boolean copiedCatLog = targetFile != null &&
                    FileTools.copyFile(fileToCopy, targetFile);
            boolean copiedJsLog = jsTargetFile != null &&
                    FileTools.copyFile(jsFileToCopy, jsTargetFile);

            if (copiedCatLog || copiedJsLog) {
                final Uri dirUri = Uri.fromFile(this.externalStorage);
                this.openDirectory(dirUri, callbackContext);
            }
        }
    }

    private void openDirectory(final Uri dirUri, final CallbackContext callbackContext) {
        final Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(dirUri, "resource/folder");

        final PackageManager packageManager = cordovaInstance.getActivity().getPackageManager();
        if (intent.resolveActivityInfo(packageManager, 0) != null) {
            cordovaInstance.getActivity().startActivity(intent);
            callbackContext.success();
        } else {
            callbackContext.error(RETURN_CODE.NO_FILE_MANAGER_FOUND.name());
        }
    }

    private boolean checkStoragePermission() {
        if (Build.VERSION.SDK_INT >= 23) {
            if (ContextCompat.checkSelfPermission(cordovaInstance.getActivity(),
                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    == PackageManager.PERMISSION_GRANTED) {
                return true;
            } else {
                Log.v(TAG, "External Storage permission is revoked");
                final String STORAGE_PERMISSIONS = Manifest.permission.WRITE_EXTERNAL_STORAGE;
                ActivityCompat.requestPermissions(cordovaInstance.getActivity(),
                        new String[]{STORAGE_PERMISSIONS}, PERMISSION_REQUEST_CODE);
                return false;
            }
        } else {
            return true;
        }
    }

    private void initLogger(final JSONArray args, final CallbackContext callbackContext) {
        try {
            final String jsFileName = args.optString(0) != null ?
                    args.optString(0) : DEFAULT_JS_FILENAME;
            final String lcFileName = args.optString(1) != null ?
                    args.optString(1) : DEFAULT_LC_FILENAME;
            this.maxFileSizeInKB = args.optString(2) != null ?
                    args.optInt(2) : DEFAULT_MAX_FILESIZE_IN_KB;
            this.filterBy = args.optString(3) == null ?
                    null : args.optString(3).split(ARRAY_SEPARATOR);
            this.filterOut = args.optString(4) == null ?
                    null : args.optString(4).split(ARRAY_SEPARATOR);
            this.enableCallback = args.optBoolean(5);

            if (jsFileName != null) {
                this.jsFile = new File(this.internalStorage, jsFileName);
                this.jsBak = FileTools.rollFile(this.jsFile, null, LOG_ROLLING_EXTENSION);
                this.jsCon = FileTools.rollFile(this.jsFile, LOG_CON_SUFFIX);

                if (this.jsFileWriter == null) {
                    this.jsFileWriter = new JsFileWriter(this.jsFile, this.jsBak,
                            this.cordovaInstance, this.maxFileSizeInKB);
                }
            }

            if (lcFileName != null) {
                this.lcFile = new File(this.internalStorage, lcFileName);
                this.lcBak = FileTools.rollFile(this.lcFile, null, LOG_ROLLING_EXTENSION);
                this.lcCon = FileTools.rollFile(this.lcFile, LOG_CON_SUFFIX);
            }

            this.zipFile = new File(this.internalStorage, DEFAULT_ZIP_FILENAME);
            callbackContext.success();
        } catch (Exception e) {
            callbackContext.error(e.getMessage());
        }
    }

    private void writeToJsLog(final JSONArray args) {
        if (this.jsFileWriter != null) {
            this.jsFileWriter.log(args.optString(0));
        }
    }

    private void startLogging(final CallbackContext callbackContext) {
        if (this.loggerThread == null && this.bashExecuter == null) {
            final LcFileWriter eventHandler = new LcFileWriter(
                    this.lcFile, this.lcBak, this.cordovaInstance,
                    this.webView, this.filterBy, this.filterOut,
                    this.maxFileSizeInKB, this.enableCallback,
                    callbackContext);
            this.bashExecuter = new BashExecutor(eventHandler);
            this.bashExecuter.setCommand(LOGCAT_COMMAND);
            this.loggerThread = new Thread(bashExecuter);
            this.loggerThread.start();
        }
    }

    private void stopLogging(final CallbackContext callbackContext) {
        if (this.bashExecuter != null) {
            this.bashExecuter.killProcess();
            this.bashExecuter = null;
        }

        if (this.loggerThread != null) {
            this.loggerThread.interrupt();
            this.loggerThread = null;
        }

        if (callbackContext != null) {
            callbackContext.success();
        }
    }

    @Override
    public void onDestroy() {
        this.stopLogging(null);
        Log.v(TAG, "Destroyed");
        super.onDestroy();
    }
}