/*
 *  Copyright 2010 Emmanuel Astier & Kevin Gaudin
 *
 *  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.acra;

import android.app.Application;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;

import com.adins.mss.base.crashlytics.FireCrash;

import org.acra.annotation.ReportsCrashes;
import org.acra.log.ACRALog;
import org.acra.log.AndroidLogDelegate;

/**
 * Use this class to initialize the crash reporting feature using
 * {@link #init(Application)} as soon as possible in your {@link Application}
 * subclass {@link Application#onCreate()} method. Configuration items must have
 * been set by using {@link ReportsCrashes} above the declaration of your
 * {@link Application} subclass.
 *
 * @author Kevin Gaudin
 */
public class ACRA {

    public static final boolean DEV_LOGGING = false; // Should be false for
    // release.
    public static final String LOG_TAG = ACRA.class.getSimpleName();
    /**
     * The key of the application default SharedPreference where you can put a
     * 'true' Boolean value to disable ACRA.
     */
    public static final String PREF_DISABLE_ACRA = "acra.disable";
    /**
     * Alternatively, you can use this key if you prefer your users to have the
     * checkbox ticked to enable crash reports. If both acra.disable and
     * acra.enable are set, the value of acra.disable takes over the other.
     */
    public static final String PREF_ENABLE_ACRA = "acra.enable";
    /**
     * The key of the SharedPreference allowing the user to disable sending
     * content of logcat/dropbox. System logs collection is also dependent of
     * the READ_LOGS permission.
     */
    public static final String PREF_ENABLE_SYSTEM_LOGS = "acra.syslog.enable";
    /**
     * The key of the SharedPreference allowing the user to disable sending his
     * device id. Device ID collection is also dependent of the READ_PHONE_STATE
     * permission.
     */
    public static final String PREF_ENABLE_DEVICE_ID = "acra.deviceid.enable";
    /**
     * The key of the SharedPreference allowing the user to always include his
     * email address.
     */
    public static final String PREF_USER_EMAIL_ADDRESS = "acra.user.email";
    /**
     * The key of the SharedPreference allowing the user to automatically accept
     * sending reports.
     */
    public static final String PREF_ALWAYS_ACCEPT = "acra.alwaysaccept";
    /**
     * The version number of the application the last time ACRA was started.
     * This is used to determine whether unsent reports should be discarded
     * because they are old and out of date.
     */
    public static final String PREF_LAST_VERSION_NR = "acra.lastVersionNr";
    public static ACRALog log = new AndroidLogDelegate();
    private static Application mApplication;

    // Accessible via ACRA#getErrorReporter().
    private static ErrorReporter errorReporterSingleton;

    // NB don't convert to a local field because then it could be garbage
    // collected and then we would have no PreferenceListener.
    private static OnSharedPreferenceChangeListener mPrefListener;
    private static ACRAConfiguration configProxy;

    /**
     * <p>
     * Initialize ACRA for a given Application. The call to this method should
     * be placed as soon as possible in the {@link Application#onCreate()}
     * method.
     * </p>
     *
     * @param app Your Application class.
     * @throws IllegalStateException if it is called more than once.
     */
    public static void init(Application app) {
        final ReportsCrashes reportsCrashes = app.getClass().getAnnotation(ReportsCrashes.class);
        if (reportsCrashes == null) {
            log.e(LOG_TAG,
                    "ACRA#init called but no ReportsCrashes annotation on Application " + app.getPackageName());
            return;
        }
        init(app, new ACRAConfiguration(reportsCrashes));
    }

    /**
     * <p>
     * Initialize ACRA for a given Application. The call to this method should
     * be placed as soon as possible in the {@link Application#onCreate()}
     * method.
     * </p>
     *
     * @param app    Your Application class.
     * @param config ACRAConfiguration to manually set up ACRA configuration.
     * @throws IllegalStateException if it is called more than once.
     */
    public static void init(Application app, ACRAConfiguration config) {
        init(app, config, true);
    }

    /**
     * <p>
     * Initialize ACRA for a given Application. The call to this method should
     * be placed as soon as possible in the {@link Application#onCreate()}
     * method.
     * </p>
     *
     * @param app                            Your Application class.
     * @param config                         ACRAConfiguration to manually set up ACRA configuration.
     * @param checkReportsOnApplicationStart Whether to invoke
     *                                       ErrorReporter.checkReportsOnApplicationStart(). Apps which adjust the
     *                                       ReportSenders should set this to false and call
     *                                       checkReportsOnApplicationStart() themselves to prevent a potential
     *                                       race with the SendWorker and list of ReportSenders.
     * @throws IllegalStateException if it is called more than once.
     */
    public static void init(Application app, ACRAConfiguration config, boolean checkReportsOnApplicationStart) {

        if (mApplication != null) {
            log.w(LOG_TAG, "ACRA#init called more than once. Won't do anything more.");
            return;
        }
        mApplication = app;

        if (config == null) {
            log.e(LOG_TAG, "ACRA#init called but no ACRAConfiguration provided");
            return;
        }
        setConfig(config);

        final SharedPreferences prefs = getACRASharedPreferences();

        try {
            checkCrashResources(config);

            log.d(LOG_TAG, "ACRA is enabled for " + mApplication.getPackageName() + ", initializing...");

            // Initialize ErrorReporter with all required data
            final boolean enableAcra = !shouldDisableACRA(prefs);
            final ErrorReporter errorReporter = new ErrorReporter(mApplication, prefs, enableAcra);

            // Append ReportSenders.
            errorReporter.setDefaultReportSenders();

            errorReporterSingleton = errorReporter;

            // Check for pending reports
            if (checkReportsOnApplicationStart) {
                errorReporter.checkReportsOnApplicationStart();
            }

        } catch (ACRAConfigurationException e) {
            log.w(LOG_TAG, "Error : ", e);
        }

        // We HAVE to keep a reference otherwise the listener could be garbage
        // collected:
        // http://stackoverflow.com/questions/2542938/sharedpreferences-onsharedpreferencechangelistener-not-being-called-consistently/3104265#3104265
        mPrefListener = new OnSharedPreferenceChangeListener() {

            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                if (PREF_DISABLE_ACRA.equals(key) || PREF_ENABLE_ACRA.equals(key)) {
                    final boolean enableAcra = !shouldDisableACRA(sharedPreferences);
                    getErrorReporter().setEnabled(enableAcra);
                }
            }
        };

        // This listener has to be set after initAcra is called to avoid a
        // NPE in ErrorReporter.disable() because
        // the context could be null at this moment.
        prefs.registerOnSharedPreferenceChangeListener(mPrefListener);
    }

    /**
     * @return the current instance of ErrorReporter.
     * @throws IllegalStateException if {@link ACRA#init(android.app.Application)} has not yet
     *                               been called.
     */
    public static ErrorReporter getErrorReporter() {
        if (errorReporterSingleton == null) {
            throw new IllegalStateException("Cannot access ErrorReporter before ACRA#init");
        }
        return errorReporterSingleton;
    }

    /**
     * Check if the application default shared preferences contains true for the
     * key "acra.disable", do not activate ACRA. Also checks the alternative
     * opposite setting "acra.enable" if "acra.disable" is not found.
     *
     * @param prefs SharedPreferences to check to see whether ACRA should be
     *              disabled.
     * @return true if prefs indicate that ACRA should be disabled.
     */
    private static boolean shouldDisableACRA(SharedPreferences prefs) {
        boolean disableAcra = false;
        try {
            final boolean enableAcra = prefs.getBoolean(PREF_ENABLE_ACRA, true);
            disableAcra = prefs.getBoolean(PREF_DISABLE_ACRA, !enableAcra);
        } catch (Exception e) {
            FireCrash.log(e);
            // In case of a ClassCastException
        }
        return disableAcra;
    }

    /**
     * Checks that mandatory configuration items have been provided.
     *
     * @throws ACRAConfigurationException if required values are missing.
     */
    static void checkCrashResources(ReportsCrashes conf) throws ACRAConfigurationException {
        switch (conf.mode()) {
            case TOAST:
                if (conf.resToastText() == 0) {
                    throw new ACRAConfigurationException(
                            "TOAST mode: you have to define the resToastText parameter in your application @ReportsCrashes() annotation.");
                }
                break;
            case NOTIFICATION:
                if (conf.resNotifTickerText() == 0 || conf.resNotifTitle() == 0 || conf.resNotifText() == 0) {
                    throw new ACRAConfigurationException(
                            "NOTIFICATION mode: you have to define at least the resNotifTickerText, resNotifTitle, resNotifText parameters in your application @ReportsCrashes() annotation.");
                }
                if (CrashReportDialog.class.equals(conf.reportDialogClass()) && conf.resDialogText() == 0) {
                    throw new ACRAConfigurationException(
                            "NOTIFICATION mode: using the (default) CrashReportDialog requires you have to define the resDialogText parameter in your application @ReportsCrashes() annotation.");
                }
                break;
            case DIALOG:
                if (CrashReportDialog.class.equals(conf.reportDialogClass()) && conf.resDialogText() == 0) {
                    throw new ACRAConfigurationException(
                            "DIALOG mode: using the (default) CrashReportDialog requires you to define the resDialogText parameter in your application @ReportsCrashes() annotation.");
                }
                break;
            default:
                break;
        }
    }

    /**
     * Retrieves the {@link SharedPreferences} instance where user adjustable
     * settings for ACRA are stored. Default are the Application default
     * SharedPreferences, but you can provide another SharedPreferences name
     * with {@link ReportsCrashes#sharedPreferencesName()}.
     *
     * @return The Shared Preferences where ACRA will retrieve its user
     * adjustable setting.
     */
    public static SharedPreferences getACRASharedPreferences() {
        ReportsCrashes conf = getConfig();
        if (!"".equals(conf.sharedPreferencesName())) {
            return mApplication.getSharedPreferences(conf.sharedPreferencesName(), conf.sharedPreferencesMode());
        } else {
            return PreferenceManager.getDefaultSharedPreferences(mApplication);
        }
    }

    /**
     * Provides the current ACRA configuration.
     *
     * @return Current ACRA {@link ReportsCrashes} configuration instance.
     */
    public static ACRAConfiguration getConfig() {
        if (configProxy == null) {
            if (mApplication == null) {
                log.w(LOG_TAG,
                        "Calling ACRA.getConfig() before ACRA.init() gives you an empty configuration instance. You might prefer calling ACRA.getNewDefaultConfig(Application) to get an instance with default values taken from a @ReportsCrashes annotation.");
            }
            configProxy = getNewDefaultConfig(mApplication);
        }
        return configProxy;
    }

    /**
     * Sets the whole ACRA configuration.
     *
     * @param conf ACRAConfiguration to use as a proxy for config info.
     */
    public static void setConfig(ACRAConfiguration conf) {
        configProxy = conf;
    }

    /**
     * @param app Your Application class.
     * @return new {@link ACRAConfiguration} instance with values initialized
     * from the {@link ReportsCrashes} annotation.
     */
    public static ACRAConfiguration getNewDefaultConfig(Application app) {
        if (app != null) {
            return new ACRAConfiguration(app.getClass().getAnnotation(ReportsCrashes.class));
        } else {
            return new ACRAConfiguration(null);
        }
    }

    /**
     * Returns true if the application is debuggable.
     *
     * @return true if the application is debuggable.
     */
    static boolean isDebuggable() {
        PackageManager pm = mApplication.getPackageManager();
        try {
            return ((pm.getApplicationInfo(mApplication.getPackageName(), 0).flags & ApplicationInfo.FLAG_DEBUGGABLE) > 0);
        } catch (NameNotFoundException e) {
            return false;
        }
    }

    static Application getApplication() {
        return mApplication;
    }

    public static void setLog(ACRALog log) {
        if (log == null) {
            throw new NullPointerException("ACRALog cannot be null");
        }
        ACRA.log = log;
    }
}
