package com.avg.zaap.crashlibrary;

import android.content.Context;
import android.util.Log;

import com.avg.zaap.crashlibrary.config.CrashLibConfig;
import com.avg.zaap.crashlibrary.config.CrashLibConfigAnnotations;
import com.avg.zaap.crashlibrary.config.EShowErrorType;
import com.avg.zaap.crashlibrary.core.helper.process.IProcessHelper;
import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.core.logger.DefaultLogger;
import com.avg.zaap.crashlibrary.core.logger.ILogger;
import com.avg.zaap.crashlibrary.offline.Connection;
import com.avg.zaap.crashlibrary.offline.Strategy;
import com.avg.zaap.crashlibrary.provider.ICrashAsyncProvider;
import com.avg.zaap.crashlibrary.provider.ICrashSyncProvider;
import com.avg.zaap.crashlibrary.provider.memory.MemoryHeapProvider;
import com.avg.zaap.crashlibrary.publish.ICrashPublisher;
import com.avg.zaap.crashlibrary.publish.cap.CapCrashPublisher;

import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import static com.avg.zaap.crashlibrary.config.EShowErrorType.TOAST;

/**
 * Initialization of Crash library config.
 * <p/>
 * Created by marek.osvald on 8. 4. 2016.
 */
public class CrashLib {

    private static final String TAG = CrashLib.class.getSimpleName();

    private static final String NATIVE_CRASH_LIBRARY = "crash";

    public static final String DEFAULT_APPLICATION_VERSION = "unknown";
    public static final EShowErrorType DEFAULT_SHOW_ERROR_TYPE = EShowErrorType.ERROR_SCREEN;
    public static final int DEFAULT_NOTIFICATION_ICON = android.R.drawable.ic_dialog_alert;
    public static final ILogger DEFAULT_LOGGER = new DefaultLogger();
    public static final String DEFAULT_CRASH_REPORT_URL = "";
    public static final String DEFAULT_CRASH_UPLOAD_URL = "";
    @SuppressWarnings("SpellCheckingInspection")
    public static final String DEFAULT_CAP_PRODUCT_ID = "ANDR";
    private static final boolean DEFAULT_DISABLE_MEMORY_HEAP_PROVIDER = false;
    private static final boolean DEFAULT_DISABLE_STACK_TRACE_CRASHING_THREAD = false;
    private static final boolean DEFAULT_DISABLE_STACK_TRACE_ALL_THREADS = false;
    private static final boolean DEFAULT_DISABLE_CAP_PUBLISHER = false;
    public static final int DEFAULT_ERROR_MESSAGE = R.string.error_message;
    public static final int DEFAULT_ERROR_DETAILS = R.string.error_details;
    private static final Strategy DEFAULT_STRATEGY = Strategy.BEST_EFFORT;
    public static final int DEFAULT_MAX_ATTEMPTS = 3;
    public static final long DEFAULT_DELAY = 60 * 60 * 1000; // milliseconds ~ 1 hour
    public static final boolean DEFAULT_OFFLINE_SUITABLE = false;
    public static final Connection DEFAULT_CONNECTION_LEVEL_SUITABLE = Connection.WIFI;
    public static final boolean DEFAULT_ROAMING_SUITABLE = false;

    public static final String CRASH_DIR = "crash";
    @SuppressWarnings("SpellCheckingInspection")
    public static final String NATIVE_CRASH_DIR = "nativecrash";

    private static CrashLibConfig sConfig;
    private static Context sContext;
    private static ICrashLibFactory sFactory;
    private static boolean sNativeLibraryLoaded = false;

    /**
     * Sets config.
     *
     * @param config config to set
     */
    public static void setConfig(CrashLibConfig config) {
        sConfig = config;
    }

    /**
     * Returns config
     *
     * @return config
     */
    public static CrashLibConfig getConfig() {
        return sConfig;
    }

    /**
     * Reads annotation from provided class and set them to config.
     * If class is not annotated, sets default.
     * 7 *
     *
     * @param context Android context
     */
    @SuppressWarnings("unused")
    public static void init(Context context) {
        init(context, new CrashLibFactory());
    }

    /**
     * Reads annotation from provided class and set them to config.
     * If class is not annotated, sets default.
     *
     * @param context Android context
     * @param factory dependency factory to be initialized with
     */
    @SuppressWarnings("unused")
    public static void init(Context context, ICrashLibFactory factory) {
        //defaults
        String applicationVersion = DEFAULT_APPLICATION_VERSION;
        EShowErrorType showErrorType = DEFAULT_SHOW_ERROR_TYPE;
        int notificationSmallIcon = DEFAULT_NOTIFICATION_ICON;
        ILogger logger = DEFAULT_LOGGER;
        CrashLogger.init(logger);
        String crashReportURL = DEFAULT_CRASH_REPORT_URL;
        String crashUploadURL = DEFAULT_CRASH_UPLOAD_URL;
        String capProductID = DEFAULT_CAP_PRODUCT_ID;
        List<ICrashSyncProvider> syncProviders = new ArrayList<>();
        List<ICrashAsyncProvider>asyncProviders = new ArrayList<>();
        List<ICrashPublisher> crashPublishers = new ArrayList<>();
        boolean disableMemoryHeapProvider = DEFAULT_DISABLE_MEMORY_HEAP_PROVIDER;
        boolean disableStackTraceCrashingThread = DEFAULT_DISABLE_STACK_TRACE_CRASHING_THREAD;
        boolean disableStackTraceAllThreads = DEFAULT_DISABLE_STACK_TRACE_ALL_THREADS;
        boolean disableCapPublisher = DEFAULT_DISABLE_CAP_PUBLISHER;
        int errorMessage = DEFAULT_ERROR_MESSAGE;
        int errorDetails = DEFAULT_ERROR_DETAILS;
        Strategy strategy = DEFAULT_STRATEGY;
        int maxAttempts = DEFAULT_MAX_ATTEMPTS;
        long delay = DEFAULT_DELAY;
        boolean offlineSuitable = DEFAULT_OFFLINE_SUITABLE;
        Connection connectionLevelSuitable = DEFAULT_CONNECTION_LEVEL_SUITABLE;
        boolean roamingSuitable = DEFAULT_ROAMING_SUITABLE;

        Class clazz = context.getClass();

        //if App class is annotated, load config from them
        if (clazz.isAnnotationPresent(CrashLibConfigAnnotations.class)) {
            CrashLibConfigAnnotations crashLibConfig = (CrashLibConfigAnnotations) clazz.getAnnotation(CrashLibConfigAnnotations.class);

            applicationVersion = crashLibConfig.applicationVersion();

            showErrorType = crashLibConfig.showErrorType();

            Class<? extends ICrashSyncProvider>[] syncProvidersClass = crashLibConfig.syncProviders();
            for (Class<? extends ICrashSyncProvider> one : syncProvidersClass) {
                try {
                    Constructor<? extends ICrashSyncProvider> constructor = one.getConstructor(Context.class);
                    ICrashSyncProvider syncProvider = constructor.newInstance(context);
                    syncProviders.add(syncProvider);
                } catch (InvocationTargetException e) {
                    CrashLogger.e(TAG, "Can't create new instance of sync provider: %s", e.getMessage());
                } catch (NoSuchMethodException e) {
                    CrashLogger.e(TAG, "Can't create new instance of sync provider because it doesn't have required constructor: %s", e.getMessage());
                } catch (InstantiationException e) {
                    // not unit tested, see newInstance javaDoc
                    CrashLogger.e(TAG, "Can't create sync provider: %s", e.getMessage());
                } catch (IllegalAccessException e) {
                    // not unit tested, see newInstance javaDoc
                    CrashLogger.e(TAG, "Illegal access during sync provider creation: %s", e.getMessage());
                }
            }

            Class<? extends ICrashAsyncProvider>[] asyncProvidersClass = crashLibConfig.asyncProviders();
            for (Class<? extends ICrashAsyncProvider> one : asyncProvidersClass) {
                try {
                    asyncProviders.add(one.newInstance());
                } catch (InstantiationException e) {
                    CrashLogger.e(TAG, "Can't create async provider: %s", e.getMessage());
                } catch (IllegalAccessException e) {
                    CrashLogger.e(TAG, "Illegal access during async provider creation: %s", e.getMessage());
                }
            }

            //add publishers
            Class<? extends ICrashPublisher>[] crashPublishersClass = crashLibConfig.crashPublishers();
            for (Class<? extends ICrashPublisher> one : crashPublishersClass) {
                try {
                    crashPublishers.add(one.newInstance());
                } catch (InstantiationException e) {
                    CrashLogger.e(TAG, "Can't create crash publisher: %s", e.getMessage());
                } catch (IllegalAccessException e) {
                    CrashLogger.e(TAG, "Illegal access during crash publisher creation: %s", e.getMessage());
                }
            }

            switch (showErrorType) {
                case NONE:
                    break;

                case TOAST:
                    errorDetails = (crashLibConfig.errorDetails() == 0) ? DEFAULT_ERROR_MESSAGE : crashLibConfig.errorDetails();
                    break;

                case NOTIFICATION:
                    errorMessage = (crashLibConfig.errorMessage() == 0) ? DEFAULT_ERROR_MESSAGE : crashLibConfig.errorMessage();
                    errorDetails = (crashLibConfig.errorDetails() == 0) ? DEFAULT_ERROR_DETAILS : crashLibConfig.errorDetails();
                    notificationSmallIcon = (crashLibConfig.notificationSmallIcon() == 0) ? DEFAULT_NOTIFICATION_ICON : crashLibConfig.notificationSmallIcon();
                    break;

                case DIALOG:
                    errorMessage = (crashLibConfig.errorMessage() == 0) ? DEFAULT_ERROR_MESSAGE : crashLibConfig.errorMessage();
                    errorDetails = (crashLibConfig.errorDetails() == 0) ? DEFAULT_ERROR_DETAILS : crashLibConfig.errorDetails();
                    break;

                case ERROR_SCREEN:
                    errorMessage = (crashLibConfig.errorMessage() == 0) ? DEFAULT_ERROR_MESSAGE : crashLibConfig.errorMessage();
                    errorDetails = (crashLibConfig.errorDetails() == 0) ? DEFAULT_ERROR_DETAILS : crashLibConfig.errorDetails();
                    // break removed, as this is the last branch
            }

            try {
                logger = crashLibConfig.logger().newInstance();
            } catch (InstantiationException e) {
                CrashLogger.e(TAG, "Can't create logger: %s", e.getMessage());
            } catch (IllegalAccessException e) {
                CrashLogger.e(TAG, "Illegal access during logger creation: %s", e.getMessage());
            }

            CrashLogger.init(logger);

            //noinspection ConstantConditions
            crashReportURL = (crashLibConfig.crashReportURL() != null && !crashLibConfig.crashReportURL().isEmpty()) ? crashLibConfig.crashReportURL() : DEFAULT_CRASH_REPORT_URL;
            //noinspection ConstantConditions
            crashUploadURL = (crashLibConfig.crashUploadURL() != null && !crashLibConfig.crashUploadURL().isEmpty()) ? crashLibConfig.crashUploadURL() : DEFAULT_CRASH_UPLOAD_URL;
            //noinspection ConstantConditions
            capProductID = (crashLibConfig.capProductID() != null && !crashLibConfig.capProductID().isEmpty()) ? crashLibConfig.capProductID() : DEFAULT_CAP_PRODUCT_ID;

            disableMemoryHeapProvider = crashLibConfig.disableMemoryHeapProvider();
            disableStackTraceAllThreads = crashLibConfig.disableStackTraceAllThreads();
            disableStackTraceCrashingThread = crashLibConfig.disableStackTraceCrashingThread();
            disableCapPublisher = crashLibConfig.disableCapPublisher();

            // add default sync providers
            if (!disableMemoryHeapProvider) {
                syncProviders.add(new MemoryHeapProvider());
            }

            // add default publishers
            if (!disableCapPublisher) {
                crashPublishers.add(new CapCrashPublisher());
            }

            strategy = crashLibConfig.strategy();
            maxAttempts = crashLibConfig.maxAttempts();
            delay = crashLibConfig.delay();

            offlineSuitable = crashLibConfig.offlineSuitable();
            connectionLevelSuitable = crashLibConfig.connectionLevelSuitable();
            roamingSuitable = crashLibConfig.roamingSuitable();
        }

        CrashLibConfig.Builder builder = new CrashLibConfig.Builder()
                .applicationVersion(applicationVersion)
                .showErrorType(showErrorType)
                .errorMessage(errorMessage)
                .errorDetails(errorDetails)
                .notificationSmallIcon(notificationSmallIcon)
                .logger(logger)
                .crashReportURL(crashReportURL)
                .crashUploadURL(crashUploadURL)
                .capProductID(capProductID)
                .syncProviders(syncProviders)
                .asyncProviders(asyncProviders)
                .publishers(crashPublishers)
                .strategy(strategy)
                .maxAttempts(maxAttempts)
                .delay(delay)
                .offlineSuitable(offlineSuitable)
                .connectionLevelSuitable(connectionLevelSuitable)
                .roamingSuitable(roamingSuitable);

        if (disableMemoryHeapProvider) {
            builder.disableMemoryHeapProvider();
        }

        if (disableStackTraceAllThreads) {
            builder.disableStackTraceAllThreads();
        }

        if (disableStackTraceCrashingThread) {
            builder.disableStackTraceCrashingThread();
        }

        if (disableCapPublisher) {
            builder.disableCapPublisher();
        }

        sConfig = builder.build();
        prepare(context, factory);
    }

    /**
     * Initializes the Crash Library with given application context, Crash Library configuration and
     * dependency factory.
     *
     * @param context        Android application context
     * @param crashLibConfig Crash Library configuration
     */
    @SuppressWarnings("unused")
    public static void init(Context context, CrashLibConfig crashLibConfig) {
        init(context, crashLibConfig, new CrashLibFactory());
    }

    /**
     * Initializes the Crash Library with given application context, Crash Library configuration and
     * dependency factory.
     *
     * @param context        Android application context
     * @param crashLibConfig Crash Library configuration
     * @param factory        dependency factory to initialize with
     */
    @SuppressWarnings("unused")
    public static void init(Context context, CrashLibConfig crashLibConfig, ICrashLibFactory factory) {
        CrashLib.sConfig = crashLibConfig;
        CrashLogger.init(crashLibConfig.getLogger());
        prepare(context, factory);
    }

    private static void prepare(Context context, ICrashLibFactory factory) {
        sContext = context;
        sFactory = factory;

        //start service and set exception handler
        IProcessHelper helper = sFactory.getProcessHelper();
        if (!helper.isReportingServiceProcess(sContext)) {
            CrashLogger.d(TAG, "Main application thread detected, starting service and initializing UncaughtExceptionHandler");
            Thread.setDefaultUncaughtExceptionHandler(sFactory.getCrashUncaughtExceptionHandler(sContext));
            sContext.startService(sFactory.getIntent(sContext, CrashReportingService.class));

            //prepare directories for logs
            createDir(CRASH_DIR);
            createDir(NATIVE_CRASH_DIR);

            if (sNativeLibraryLoaded) {
                initializeNativeLibrary(sContext.getFilesDir().getAbsolutePath() + File.separatorChar + NATIVE_CRASH_DIR);
            }
        } else {
            CrashLogger.d(TAG, "Service thread detected.");
        }
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private static void createDir(String dir) {
        File crash = sFactory.getFile(sContext.getFilesDir(), dir);
        if (crash.exists()) {
            if (!crash.isDirectory()) {
                crash.delete();
                crash.mkdir();
                CrashLogger.d(TAG, "File (not dir) %s removed. Dir %s created.", crash.getAbsolutePath(), crash.getAbsolutePath());
            } else {
                CrashLogger.d(TAG, "Dir %s already exists.", crash.getAbsolutePath());
            }
        } else {
            crash.mkdirs();
            CrashLogger.d(TAG, "Dir %s created.", crash.getAbsolutePath());
        }
    }

    static {
        try {
            System.loadLibrary(NATIVE_CRASH_LIBRARY);
            sNativeLibraryLoaded = true;
            Log.d(TAG, "Native library crash successfully loaded.");
        } catch (UnsatisfiedLinkError ex) {
            CrashLogger.e(TAG, "Unable to load native library: crash");
        }
    }

    @SuppressWarnings("JniMissingFunction")
    private static native void initializeNativeLibrary(String crashDir);

}
