package com.avg.zaap.crashlibrary;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.FileObserver;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.v4.app.NotificationCompat;
import android.widget.Toast;

import com.avg.zaap.crashlibrary.config.EShowErrorType;
import com.avg.zaap.crashlibrary.core.logger.CrashLogger;
import com.avg.zaap.crashlibrary.file.observer.IFilePathObserverCallback;
import com.avg.zaap.crashlibrary.provider.ICrashAsyncProvider;
import com.avg.zaap.crashlibrary.provider.ICrashProviderCallback;
import com.avg.zaap.crashlibrary.publish.cap.ICapHelper;
import com.avg.zaap.crashlibrary.show.error.activity.ErrorActivity;
import com.avg.zaap.crashlibrary.show.error.dialog.DialogActivity;

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

/**
 * This class handles the recoverable and unrecoverable Android
 * <p>
 * Created by marek.osvald on 2.5.2016.
 */
public class CrashReporter implements ICrashReporter, ICrashReporterCallback, IFilePathObserverCallback, IServiceConnectionCallback {

    private static final String TAG = CrashReporter.class.getSimpleName();
    private static final int ZERO_OFFSET = 0;
    private static final String MODULE_NAME_NATIVE = "native";
    private static final String PROCESS_NAME_NATIVE = "native";
    private static final int NO_FLAGS = 0;

    @SuppressWarnings("SpellCheckingInspection")
    private static final String DIRECTORY_NATIVE_CRASH = "nativecrash";
    private static final int NOTIFICATION_ID = 132;

    private Context mContext;
    private FileObserver mFileObserver;
    private NotificationCompat.Builder mNotificationCompatBuilder;
    private ICrashReporterFactory mFactory;
    private List<String> mFilePathsToPublish;
    private ServiceConnection mServiceConnection;
    private ICapHelper mCapHelper;

    /**
     * Parametric constructor. Sets service context to work with. Uses default dependency factory.
     *
     * @param context service context to work with
     */
    public CrashReporter(@NonNull Context context) {
        this(context, new CrashReporterFactory());
    }

    /**
     * Parametric constructor. Sets service context to work with and dependency factory to be used.
     *
     * @param context service context to work with
     * @param factory dependency factory to be used
     */
    public CrashReporter(@NonNull Context context, ICrashReporterFactory factory) {
        mContext = context;
        mFileObserver = factory.getFilePathObserver(context.getFilesDir().getAbsolutePath() + File.separatorChar + DIRECTORY_NATIVE_CRASH + File.separatorChar, this);
        mNotificationCompatBuilder = factory.getNotificationCompatBuilder(context);
        mCapHelper = factory.getCapHelper();
        mFactory = factory;
    }

    @Override
    public void reportAndroidCrash(@NonNull ReportData reportData) {
        CrashLogger.e(TAG, String.format("Android crash reported, crashCode: %s", reportData.getCrashCode()));

        final File externalFilesDir = mContext.getExternalFilesDir(null);

        if (externalFilesDir != null && externalFilesDir.exists()) {
            mFilePathsToPublish = reportData.getFilesToPack();

            //read config which error to show
            final EShowErrorType showErrorType = CrashLib.getConfig().getShowErrorType();
            CrashLogger.d(TAG, "Show error type: " + showErrorType);

            //show error
            notifyUserAboutCrash(CrashLib.getConfig().getShowErrorType(), reportData);

            //call all asynchronous providers
            callAsyncProviders(reportData);
        } else {
            CrashLogger.e(TAG, "Unable to publish crash data, external files dir is null.");
        }
    }

    @Override
    public void startWatchingNativeCrashReports() {
        CrashLogger.d(TAG, "startWatchingNativeCrashReports() called");
        mFileObserver.startWatching();
    }

    @Override
    public void stopWatchingNativeCrashReports() {
        CrashLogger.d(TAG, "stopWatchingNativeCrashReports() called");
        mFileObserver.stopWatching();
    }

    @Override
    public void publishResults(final ReportData reportData) {
        CrashLogger.d(TAG, "publishResults() called");

        Thread thread = mFactory.getThread(new Runnable() {

            @Override
            public void run() {
                Intent intent = mFactory.getIntent(mContext, CrashReportingService.class);
                mServiceConnection = mFactory.getServiceConnection(reportData, CrashReporter.this);
                mContext.bindService(intent, mServiceConnection, NO_FLAGS);
            }

        });

        thread.start();
    }

    @Override
    public void onNewFileCreated(@NonNull String crashCode, @NonNull String path) {
        CrashLogger.d(TAG, "New file %s created in: %s", crashCode, path);

        ReportData reportData = mFactory.getReportData();

        // common
        reportData.setCrashCode(crashCode);
        reportData.setErrorName(null);
        reportData.setErrorMessage(null);
        reportData.setErrorDetails(null);
        List<String> filesToPack = new ArrayList<>();
        filesToPack.add(path);
        reportData.setFilesToPack(filesToPack);

        // CAP
        reportData.setCapExceptionCode(mCapHelper.getExceptionCodeFromCrashCode(crashCode));
        reportData.setCapFaultingOffset(ZERO_OFFSET);
        reportData.setCapModuleName(MODULE_NAME_NATIVE);
        reportData.setCapModuleVersion(CrashLib.getConfig().getApplicationVersion());
        reportData.setCapProcessName(PROCESS_NAME_NATIVE);
        reportData.setCapProcessVersion(BuildConfig.VERSION_NAME);
        reportData.setCapGUID(UUID.randomUUID().toString());

        // offline
        reportData.setPublishAttempts(0);
        reportData.setLastAttemptTimestamp(0);

        File externalFilesDir = mContext.getExternalFilesDir(null);

        if (externalFilesDir != null && externalFilesDir.exists()) {
            mFilePathsToPublish = new ArrayList<>(CrashLib.getConfig().getAsyncProviders().size() + 1);
            mFilePathsToPublish.add(path);
            notifyUserAboutCrash(CrashLib.getConfig().getShowErrorType(), reportData);
            callAsyncProviders(reportData);
        } else {
            CrashLogger.e(TAG, "Unable to publish crash data, external files dir is null.");
        }
    }

    private void callAsyncProviders(final ReportData reportData) {
        String pathToLogsDirectory = mContext.getFilesDir().getAbsolutePath();
        List<ICrashAsyncProvider> asyncProviders = CrashLib.getConfig().getAsyncProviders();
        final int asyncProviderSize = asyncProviders.size();

        // reset callback handler
        ICrashProviderCallback crashProviderCallbackHandler = mFactory.getCrashPublisherCallbackHandler(reportData, this);

        if (asyncProviderSize > 0) {
            //call all asynchronous providers
            for (ICrashAsyncProvider asyncProvider : asyncProviders) {
                asyncProvider.provideData(pathToLogsDirectory, crashProviderCallbackHandler);
            }
        } else {
            // no async providers, publish data
            publishResults(reportData);
        }
    }

    private void notifyUserAboutCrash(final EShowErrorType errorType, final ReportData reportData) {
        Handler handler = mFactory.getHandler(Looper.getMainLooper());
        handler.post(new Runnable() {

            @Override
            public void run() {
                switch (errorType) {
                    case TOAST:
                        showToast();
                        break;

                    case DIALOG:
                        showDialog(reportData);
                        break;

                    case ERROR_SCREEN:
                        showErrorActivity(reportData);
                        break;

                    case NOTIFICATION:
                        showNotification();
                        break;
                }
            }

        });
    }

    private void showToast() {
        Toast.makeText(mContext, CrashLib.getConfig().getErrorDetails(), Toast.LENGTH_SHORT).show();
    }

    private void showDialog(ReportData reportData) {
        showActivity(DialogActivity.class, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION, reportData);
    }

    private void showErrorActivity(ReportData reportData) {
        showActivity(ErrorActivity.class, Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK, reportData);
    }

    private void showActivity(Class clazz, int flags, ReportData reportData) {
        Intent intent = mFactory.getIntent(mContext, clazz);
        intent.putExtra(ErrorActivity.INTENT_KEY_REPORT_DATA, reportData);
        intent.setFlags(intent.getFlags() | flags);
        mContext.startActivity(intent);
    }

    private void showNotification() {
        final Notification notification = mNotificationCompatBuilder
                .setSmallIcon(CrashLib.getConfig().getNotificationSmallIcon())
                .setContentTitle(mContext.getString(CrashLib.getConfig().getErrorMessage()))
                .setContentText(mContext.getString(CrashLib.getConfig().getErrorDetails()))
                .build();

        NotificationManager mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
        mNotificationManager.notify(NOTIFICATION_ID, notification);
    }

    @Override
    public void onCrashPublished() {
        CrashLogger.d(TAG, "onCrashPublished() called");
        mContext.unbindService(mServiceConnection);
    }

    @Override
    public void onCrashPublishingFailed() {
        CrashLogger.e(TAG, "onCrashPublishingFailed() called");
        mContext.unbindService(mServiceConnection);
    }

}
