package com.mobify.astro;

import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;

import com.mobify.astro.dialogs.DialogManager;
import com.mobify.astro.dialogs.ViewFragment;
import com.mobify.astro.messaging.EventManager;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.utilities.LocalizationUtilities;
import com.squareup.seismic.ShakeDetector;

import org.apache.cordova.CordovaActivity;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginEntry;

import org.json.JSONObject;

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

import dalvik.system.DexFile;

public class AstroActivity extends CordovaActivity implements PermissionsInterface {
    private static final String TAG = AstroActivity.class.getName();

    private final String SAVED_INSTANCE_STATE_CONSUMED_INTENT = "SAVED_INSTANCE_STATE_CONSUMED_INTENT";

    protected AstroApplication application;
    protected LocalizationWrapper localizationWrapper;
    protected SettingsStore settingsStore;
    protected CookiesStore cookiesStore;
    private AstroPlugin mainViewPlugin;
    private boolean isResuming;
    private Bundle savedState;
    protected DeepLinkHandler deepLinkHandler;

    // Used for the content view of the Activity
    // The MainViewPlugin's view will be placed as a child in this container
    // When fragments are displayed in the activity they end up as children of this container
    protected FrameLayout dialogContainer;

    private DialogManager dialogManager;

    private SoftInputLayoutManager softInputLayoutManager;

    private ShakeDetector shakeDetector;

    private boolean launchImageEnabled = true;
    private int backgroundColor = Color.WHITE;

    protected PluginManager pluginManager;
    protected EventManager eventManager;
    protected MessageSender messageSender;
    protected LocalizationUtilities localizationUtilities;

    protected AstroActivityClient client = null;
    protected AstroActivityShakeListener shakeListener = new AstroActivityShakeListener();
    protected PermissionsManager permissionsManager;

    @Override
    public void permissionsRequest(int requestCode, String[] permissions, PermissionsManager.PermissionsCallback callback) {
        permissionsManager.permissionRequest(requestCode, permissions, callback);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (!permissionsManager.handleRequestPermissionsResult(requestCode, permissions, grantResults)) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    protected interface AstroActivityClient {
        void cordovaLoaded();
    }

    protected class AstroActivityShakeListener implements ShakeDetector.Listener {
        /**
         * Called when the shake detector detects a shake event
         */
        @Override
        public void hearShake() {
            application.togglePreview(new JSONObject());
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (savedInstanceState != null) {
            isResuming = true;
            savedState = savedInstanceState;
        } else {
            isResuming = false;
        }

        eventManager = new EventManager();
        localizationUtilities = new LocalizationUtilities(this);
        messageSender = new MessageSender(eventManager);
        pluginManager = new PluginManager(this, eventManager, messageSender);
        application = new AstroApplication(this, eventManager, messageSender);
        dialogManager = new DialogManager(this, messageSender);

        autoRegisterPlugins();

        if (BuildConfig.ASTRO_PREVIEW) {
            shakeDetector = new ShakeDetector(this.shakeListener);
            shakeDetector.start((SensorManager) getSystemService(Context.SENSOR_SERVICE));
        }

        deepLinkHandler = new DeepLinkHandler(this, application);
        boolean consumedIntent = false;
        if (savedInstanceState != null && savedInstanceState.containsKey(SAVED_INSTANCE_STATE_CONSUMED_INTENT)) {
            consumedIntent = savedInstanceState.getBoolean(SAVED_INSTANCE_STATE_CONSUMED_INTENT);
        }
        deepLinkHandler.setConsumedIntent(consumedIntent);
        deepLinkHandler.setAppStartedWithDeepLink(true);

        dialogContainer = new FrameLayout(this);
        dialogContainer.setId(R.id.main_view_id);
        dialogContainer.setBackgroundColor(Color.TRANSPARENT);

        // We have to delay adding the dialogContainer to the Activity
        // because Cordova clobbers it by calling setContentView() itself
        // when it boots up (later on).

        localizationWrapper = new LocalizationWrapper(getLocalizationUtilities(), eventManager, messageSender);
        settingsStore = new SettingsStore(this, eventManager, messageSender);
        cookiesStore = new CookiesStore(eventManager, messageSender);
        permissionsManager = new PermissionsManager(this);
        softInputLayoutManager = new SoftInputLayoutManager(this);

        super.onCreate(savedInstanceState);
    }

    @Override
    public void onPause() {
        pluginManager.onPause();
        application.appDeactivated();

        if (BuildConfig.ASTRO_PREVIEW) {
            shakeDetector.stop();
        }

        eventManager.onPause(); // Must do afterwards because application.appDeactivated sends an event
        super.onPause();
    }

    @Override
    public void onResume() {
        eventManager.onResume();
        application.appActivated();

        if (BuildConfig.ASTRO_PREVIEW) {
            shakeDetector.start((SensorManager) getSystemService(Context.SENSOR_SERVICE));
        }

        super.onResume();

        deepLinkHandler.attemptDeepLinkHandling(getIntent());
    }

    @Override
    public void onSaveInstanceState(Bundle outBundle) {
        pluginManager.onSaveInstanceState(outBundle);
        outBundle.putBoolean(SAVED_INSTANCE_STATE_CONSUMED_INTENT, deepLinkHandler.getConsumedIntent());

        super.onSaveInstanceState(outBundle);
    }

    @Override
    public void onDestroy() {
        pluginManager.onDestroy();

        if (BuildConfig.ASTRO_PREVIEW) {
            shakeDetector.stop();
        }
        eventManager.onDestroy();
        super.onDestroy();
    }

    @Override
    public void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);

        deepLinkHandler.setConsumedIntent(false);
        deepLinkHandler.setAppStartedWithDeepLink(false);
    }

    @Override
    protected void loadConfig() {
        // We override this method to load custom Cordova configuration without resorting to an XML
        // file.
        super.loadConfig();

        // Set `ShowTitle` to true so that Cordova does not disable the action bar.
        preferences.set("ShowTitle", true);
    }

    @Override
    public void onBackPressed() {
        if (getFragmentManager().getBackStackEntryCount() <= 1) {
            application.backButtonPressed(new JSONObject());
        } else {
            super.onBackPressed();
        }
    }

    @Override
    protected void createViews() {
        super.createViews();
        updateCordovaWebViewVisibility(View.INVISIBLE);
    }

    /**
     * Gets the plugin that is set as the main view
     * @return plugin that is set as the main view
     */
    public AstroPlugin getMainViewPlugin() {
        return mainViewPlugin;
    }

    protected CordovaWebView getCordovaWebView() {
        return appView;
    }

    /**
     * Gets the DialogManager for this activity
     */
    public DialogManager getDialogManager() {
        return dialogManager;
    }

    /**
     * Get the bundle the activity was restored with
     * @return a bundle that was used to restore the saved instance state
     */
    public Bundle getSavedState() {
        return savedState;
    }

    /**
     * Returns true if this activity got launched with a savedInstanceState bundle
     * @return true if this activity got launched with a savedInstanceState bundle
     */
    public boolean isResuming() {
        return isResuming;
    }

    /**
     * Setter for the pointer to the mainViewPlugin
     * @param address The address of the AstroPlugin to set as the main view
     */
    public void setMainViewPlugin(String address) {
        AstroPlugin plugin = pluginManager.instanceForAddress(address);
        View pluginView = plugin.getView();

        if (launchImageEnabled) {
            pluginView.setVisibility(View.INVISIBLE);
        }

        // If we have already set a main view plugin before,
        // we are replacing it and need to remove the old one
        if (mainViewPlugin != null) {
            dialogContainer.removeView(mainViewPlugin.getView());
        }

        dialogContainer.addView(pluginView, 0, new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
        ));

        mainViewPlugin = plugin;
    }

    // Adds the dialogContainer to the view hierarchy if not already added
    public void addDialogContainerToHierarchy() {
        if (dialogContainer.getParent() == null) {
            // Now we can place the dialogContainer into the Activity (see onCreate())
            addContentView(dialogContainer, new FrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
            ));
        }
        softInputLayoutManager.addSoftInputLayoutListener();
    }

    public void dismissLaunchImage() {
        if (!launchImageEnabled) {
            return;
        }

        launchImageEnabled = false;
        dialogContainer.setBackgroundColor(backgroundColor);
        if (mainViewPlugin != null && mainViewPlugin.getView() != null) {
            mainViewPlugin.getView().setVisibility(View.VISIBLE);
        }
        updateCordovaWebViewVisibility(View.VISIBLE);
    }

    public void hideKeyboard() {
        application.hideKeyboard();
    }

    public void setBackgroundColor(int color) {
        backgroundColor = color;

        if (launchImageEnabled) {
            return;
        }

        dialogContainer.setBackgroundColor(backgroundColor);
    }

    private void updateCordovaWebViewVisibility(int visibility) {
        CordovaWebView cordovaWebView = appView;
        if (cordovaWebView != null) {
            cordovaWebView.getView().setVisibility(visibility);
        }
    }

    private void autoRegisterPlugins() {
        String astroPackageName = AstroApplication.class.getPackage().getName();
        String customPackageName = this.getPackageName();

        try {
            List<String> potentialPluginClasses = getPotentialPluginClasses();
            List<String> potentialCustomPluginClasses = new ArrayList<>();
            for (String potentialPluginClass: potentialPluginClasses) {
                Log.d(TAG, "Checking class: " + potentialPluginClass);
                if (potentialPluginClass.startsWith(astroPackageName)) {
                    tryRegisterPluginClass(potentialPluginClass);
                } else if (potentialPluginClass.startsWith(customPackageName)) {
                    potentialCustomPluginClasses.add(potentialPluginClass);
                }
            }
            // Custom plugins are registered last so that they can
            // override Astro plugins of the same name
            for (String potentialCustomPluginClass: potentialCustomPluginClasses) {
                tryRegisterPluginClass(potentialCustomPluginClass);
            }
        } catch (IOException ex) {
            Log.e(TAG, "autoRegisterPlugins: Error registering plugins!", ex);
        }
    }

    void tryRegisterPluginClass(String className) {
        try {
            Class pluginClass = Class.forName(className);
            if (AstroPlugin.class.isAssignableFrom(pluginClass)) {
                // Found a plugin!!
                Log.d(TAG, String.format("Registering Astro plugin: %s", className));

                // Assigning pluginClass to a variable of the correct generic type expected by
                // register() method so that warning can be suppressed. The isAssignableFrom check
                // above verifies that pluginClass extends AstroPlugin.class.
                @SuppressWarnings("unchecked")
                Class<? extends AstroPlugin> astroPluginClass = pluginClass;
                pluginManager.register(astroPluginClass);
            }
        } catch (ClassNotFoundException cnfe) {
            Log.e(TAG, "tryRegisterPluginClass: Error checking if type '" + className + "' is an Astro plugin.", cnfe);
        }
    }

    List<String> getPotentialPluginClasses() throws IOException {
        List<String> classNames = new ArrayList<>();

        if (BuildConfig.DEBUG) {
            // "instant-run" only exists in Debug builds. It results in the app
            // classes being in 1, or more, files in the `instant-run/dex` directory.
            File instantRunDir = new File(getBaseContext().getFilesDir(), "instant-run/dex");
            if (instantRunDir.exists()) {
                for (File dexPath : instantRunDir.listFiles()) {
                    classNames.addAll(listClassesInDexFile(dexPath.getAbsolutePath()));
                }
            }
        }

        // Release (or non instant-run) build (we look through the actual .apk)
        classNames.addAll(listClassesInDexFile(getApplicationInfo().sourceDir));

        return classNames;
    }

    private List<String> listClassesInDexFile(String dexPath) {
        DexFile dex = null;
        List<String> classNames = new ArrayList<>();

        try {
            dex = new DexFile(dexPath);
            for (Enumeration<String> entries = dex.entries(); entries.hasMoreElements(); ) {
                classNames.add(entries.nextElement());
            }
        } catch (IOException ex) {
            Log.e(TAG, "Error listing classes in DEX file at: " + dexPath + "!", ex);
        } finally {
            try {
                if (dex != null) {
                    dex.close();
                }
            } catch (Exception ex) {
                // Can't do anything about error occurring from closing the Dex File
            }
        }

        return classNames;
    }

    /*******************************************
     * Dialog support
     */
    public Fragment getFragmentForId(int fragmentId) {
        return getFragmentManager().findFragmentByTag(Integer.toString(fragmentId));
    }

    public void setViewForFragment(int fragmentId, View view) {
        ViewFragment fragment = (ViewFragment) getFragmentForId(fragmentId);
        if (fragment != null && view != null) {
            fragment.setContentView(view);
            fragment.refresh();
        }
    }

    public View getViewForFragment(int fragmentId) {
        ViewFragment fragment = (ViewFragment)getFragmentForId(fragmentId);
        if (fragment != null) {
            return fragment.getContentView();
        }
        return null;
    }

    public void showNewFragment(Fragment newFragment, String name, boolean animated) {
        // When we are testing, Cordova does not have time to fully initialize so the dialogContainer
        // does not get added to the view. This ensures that the dialogContainer is always in the
        // view when we need it to be. After Cordova starts, everything from the view will be removed
        // and the dialogContainer will be re-added
        addDialogContainerToHierarchy();

        FragmentTransaction transaction = getFragmentManager().beginTransaction();

        if (animated) {
            transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
        }

        transaction.add(dialogContainer.getId(), newFragment, name);
        transaction.addToBackStack(name);

        transaction.commit();
    }

    public void hide(int fragmentId) {
        Fragment fragment = getFragmentForId(fragmentId);
        if (fragment == null) {
            return;
        }

        FragmentManager fragmentManager = getFragmentManager();

        fragmentManager.popBackStackImmediate(Integer.toString(fragmentId), FragmentManager.POP_BACK_STACK_INCLUSIVE);
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.remove(fragment);
        transaction.commit();
    }

    public void displayViewFragment(View view, int fragmentId, boolean animated) {
        if (view == null) {
            return;
        }

        ViewFragment newFragment = new ViewFragment();
        newFragment.setContentView(view);

        showNewFragment(newFragment, Integer.toString(fragmentId), animated);
    }

    public void displayDialogFragment(DialogFragment fragment, int fragmentId) {
        fragment.show(getFragmentManager(), Integer.toString(fragmentId));
    }

    /*
     * End of Dialog Support
     ****************************************************************/

    public LocalizationUtilities getLocalizationUtilities() {
        return localizationUtilities;
    }

    protected void startCordova() {
        // Use the plugin infrastructure to detect when the page is loaded!
        super.pluginEntries.add(new PluginEntry("astro", new CordovaPlugin() {
            @Override
            public Object onMessage(String id, Object data) {
                if (id.equals("onPageFinished") && data.equals(launchUrl)) {
                    assert client != null;

                    addDialogContainerToHierarchy();

                    // Notify so we can boot Astro
                    client.cordovaLoaded();
                }

                return null;
            }
        }));

        // this.launchUrl should be loaded from res/xml/config.xml
        loadUrl(launchUrl);
    }

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(LocalizationUtilities.updateContextWithCurrentLocalization(newBase));
    }
}
