package com.hokolinks.deeplinking;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;

import com.hokolinks.Hoko;
import com.hokolinks.deeplinking.annotations.DeeplinkDefaultRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkFragmentActivity;
import com.hokolinks.deeplinking.annotations.DeeplinkMetadata;
import com.hokolinks.deeplinking.annotations.DeeplinkMultipleRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkQueryParameter;
import com.hokolinks.deeplinking.annotations.DeeplinkRoute;
import com.hokolinks.deeplinking.annotations.DeeplinkRouteParameter;
import com.hokolinks.model.Deeplink;
import com.hokolinks.model.IntentRouteImpl;
import com.hokolinks.model.Route;
import com.hokolinks.model.exceptions.ActivityNotDeeplinkableException;
import com.hokolinks.utils.log.HokoLog;

import org.json.JSONException;
import org.json.JSONObject;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * HokoAnnotation parser serves the purpose of analyzing the Activity classes on the application
 * and determining their deeplinking capabilities based on the given Hoko annotations.
 */
public class AnnotationParser {

    // Route link

    /**
     * Get the route annotated with DeeplinkRoute on a certain class.
     *
     * @param classObject A classObject (usually an activity).
     * @return The route string.
     */
    private static String routeFromClass(Class classObject) {
        DeeplinkRoute annotation = (DeeplinkRoute) classObject
                .getAnnotation(DeeplinkRoute.class);
        if (annotation != null && !annotation.value().equals(DeeplinkRoute.noValue)) {
            return annotation.value();
        }
        return null;
    }

    /**
     * Get the route annotated with DeeplinkMultipleRoute on a certain class.
     *
     * @param classObject A classObject (usually an activity).
     * @return The route string.
     */
    private static List<String> routesFromClass(Class classObject) {
        DeeplinkMultipleRoute annotation = (DeeplinkMultipleRoute) classObject
                .getAnnotation(DeeplinkMultipleRoute.class);
        if (annotation != null && annotation.routes().length > 0) {
            return Arrays.asList(annotation.routes());
        }
        return null;
    }


    // Generating

    /**
     * This generates a Deeplink instance from an Activity instance, taking basis on the
     * annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
     *
     * @param activity An annotated activity.
     * @return A Deeplink instance or null.
     */
    public static Deeplink deeplinkFromActivity(Activity activity) {
        return deeplinkFromObject(activity);
    }

    /**
     * This generates a Deeplink instance from a Fragment instance, taking basis on the
     * annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
     *
     * @param fragment An annotated fragment.
     * @return A Deeplink instance or null.
     */
    public static Deeplink deeplinkFromFragment(Fragment fragment) {
        return deeplinkFromObject(fragment);
    }

    /**
     * This generates a Deeplink instance from a Fragment instance, taking basis on the
     * annotations within. It collects values, propagates the hashmaps and returns a Deeplink.
     *
     * @param fragment An annotated fragment.
     * @return A Deeplink instance or null.
     */
    public static Deeplink deeplinkFromFragment(android.app.Fragment fragment) {
        return deeplinkFromObject(fragment);
    }

    private static Deeplink deeplinkFromObject(Object object) {
        String route = routeFromClass(object.getClass());
        if (route != null) {
            HashMap<String, String> routeParameters = getRouteParametersFromInstance(object);
            HashMap<String, String> queryParameters = getQueryParametersFromInstance(object);
            if (routeParameters == null) {
                HokoLog.e(new ActivityNotDeeplinkableException(object.getClass().getName()));
                return null;
            }
            return Deeplink.deeplink(route, routeParameters, queryParameters);
        } else {
            HokoLog.e(new ActivityNotDeeplinkableException(object.getClass().getName()));
        }
        return null;
    }

    /**
     * Returns instantiated route parameters from an instantiated activity or fragment object.
     * Retrieves all fields annotated with the DeeplinkRouteParameter annotations along with their
     * values. Will return null if one value is either null or could not be retrieved.
     *
     * @param object An annotated object (either an Activity or a Fragment).
     * @return A HashMap with route components as keys and route parameters as values.
     */
    private static HashMap<String, String> getRouteParametersFromInstance(Object object) {
        HashMap<String, Field> routeParameterFields = getRouteParameters(object.getClass());
        HashMap<String, String> routeParameters = new HashMap<>();
        for (String key : routeParameterFields.keySet()) {
            Field field = routeParameterFields.get(key);
            String value = getValueForField(field, object);
            if (value != null) {
                routeParameters.put(key, value);
            } else {
                return null;
            }
        }
        return routeParameters;
    }

    /**
     * Returns instantiated query parameters from an instantiated activity or fragment object.
     * Retrieves all fields annotated with the DeeplinkQueryParameter annotations along with their
     * values. Will always return because query parameters are not required.
     *
     * @param object An annotated object (either an Activity or a Fragment).
     * @return A HashMap with query components as keys and query parameters as values.
     */
    private static HashMap<String, String> getQueryParametersFromInstance(Object object) {
        HashMap<String, Field> queryParameterFields = getQueryParameters(object.getClass());
        HashMap<String, String> queryParameters = new HashMap<>();
        for (String key : queryParameterFields.keySet()) {
            Field field = queryParameterFields.get(key);
            String value = getValueForField(field, object);
            if (value != null) {
                queryParameters.put(key, value);
            }
        }
        return queryParameters;
    }

    // Injecting

    /**
     * Injects route parameters on to the fields of an activity instance, from an inbound intent.
     * Uses a route to map parameters to keys and then to the correct fields on the activity
     * object.
     *
     * @param activity An annotated activity.
     * @return true in case it injected values, false otherwise.
     */
    public static boolean inject(Activity activity) {
        String route = activity.getIntent().getStringExtra(IntentRouteImpl.BUNDLE_KEY);
        if (route == null)
            return false;

        Bundle routeParametersBundle = activity.getIntent()
                .getBundleExtra(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
        Bundle queryParametersBundle = activity.getIntent()
                .getBundleExtra(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
        String metadata = activity.getIntent().getStringExtra(IntentRouteImpl.METADATA_KEY);

        String routeFromClass = routeFromClass(activity.getClass());
        List<String> routesFromClass = routesFromClass(activity.getClass());
        boolean isClassRoute = (routeFromClass != null && routeFromClass.equals(route)) ||
                (routesFromClass != null && routesFromClass.contains(route));
        if (isClassRoute) { // Activity
            return inject(activity, route, routeParametersBundle, queryParametersBundle, metadata);
        } else if (activity instanceof FragmentActivity) {
            return injectFragment((FragmentActivity) activity, route, routeParametersBundle,
                    queryParametersBundle, metadata);
        }
        return false;
    }

    /**
     * Injects route parameters on to the fields of an fragment instance, from inbound arguments.
     * Uses a route to map parameters to keys and then to the correct fields on the fragment
     * object.
     *
     * @param fragment An annotated fragment.
     * @return true in case it injected values, false otherwise.
     */
    public static boolean inject(Fragment fragment) {
        if (fragment.getArguments() == null)
            return false;

        String route = fragment.getArguments().getString(IntentRouteImpl.BUNDLE_KEY);
        if (route == null)
            return false;

        Bundle routeParametersBundle = fragment.getArguments()
                .getBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
        Bundle queryParametersBundle = fragment.getArguments()
                .getBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
        String metadata = fragment.getArguments().getString(IntentRouteImpl.METADATA_KEY);

        return inject(fragment, route, routeParametersBundle, queryParametersBundle, metadata);
    }

    /**
     * Injects route parameters on to the fields of an fragment instance, from inbound arguments.
     * Uses a route to map parameters to keys and then to the correct fields on the fragment
     * object.
     *
     * @param fragment An annotated fragment.
     * @return true in case it injected values, false otherwise.
     */
    @TargetApi(11)
    public static boolean inject(android.app.Fragment fragment) {
        if (fragment.getArguments() == null)
            return false;

        String route = fragment.getArguments().getString(IntentRouteImpl.BUNDLE_KEY);
        if (route == null)
            return false;

        Bundle routeParametersBundle = fragment.getArguments()
                .getBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY);
        Bundle queryParametersBundle = fragment.getArguments()
                .getBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY);
        String metadata = fragment.getArguments().getString(IntentRouteImpl.METADATA_KEY);

        return inject(fragment, route, routeParametersBundle, queryParametersBundle, metadata);
    }

    /**
     * Injects a FragmentActivity with a possible deeplinkable fragment, its route parameters and
     * its query parameters.
     *
     * @param activity              A FragmentActivity object.
     * @param route                 A route string.
     * @param routeParametersBundle A bundle containing the route parameters.
     * @param queryParametersBundle A bundle containing the query parameters.
     * @return true in case it injected values, false otherwise.
     */
    private static boolean injectFragment(FragmentActivity activity, String route,
                                          Bundle routeParametersBundle,
                                          Bundle queryParametersBundle,
                                          String metadata) {
        DeeplinkFragmentActivity deeplinkFragmentActivityAnnotation =
                getFragmentAnnotationFromClass(activity.getClass());

        if (deeplinkFragmentActivityAnnotation == null
                || deeplinkFragmentActivityAnnotation.fragments().length == 0)
            return false;

        Class[] fragmentClasses = deeplinkFragmentActivityAnnotation.fragments();
        Class<?> fragmentClass = findFragmentForRoute(route, fragmentClasses);

        if (fragmentClass == null)
            return false;

        try {
            Bundle bundle = new Bundle();
            bundle.putString(IntentRouteImpl.BUNDLE_KEY, route);
            bundle.putBundle(IntentRouteImpl.ROUTE_PARAMETERS_BUNDLE_KEY, routeParametersBundle);
            bundle.putBundle(IntentRouteImpl.QUERY_PARAMETERS_BUNDLE_KEY, queryParametersBundle);
            bundle.putString(IntentRouteImpl.METADATA_KEY, metadata);
            if (Fragment.class.isAssignableFrom(fragmentClass)) {
                Fragment fragment = (Fragment) fragmentClass.getDeclaredConstructor().newInstance();
                fragment.setArguments(bundle);
                FragmentManager fragmentManager = activity.getSupportFragmentManager();
                fragmentManager.beginTransaction()
                        .replace(deeplinkFragmentActivityAnnotation.id(), fragment).commit();
                return true;
            } else if (android.app.Fragment.class.isAssignableFrom(fragmentClass)) {
                android.app.Fragment fragment = (android.app.Fragment) fragmentClass
                        .getDeclaredConstructor().newInstance();
                fragment.setArguments(bundle);
                android.app.FragmentManager fragmentManager = activity.getFragmentManager();
                fragmentManager.beginTransaction()
                        .replace(deeplinkFragmentActivityAnnotation.id(), fragment).commit();
                return true;
            }
        } catch (Exception e) {
            HokoLog.e(e);
        }

        return false;
    }

    /**
     * Injects a route, route parameters and query parameters to a given annotated object.
     *
     * @param object                The annotated object.
     * @param route                 The given route.
     * @param routeParametersBundle A bundle containing the route parameters.
     * @param queryParametersBundle A bundle containing the query parameters.
     * @return true in case it injected values, false otherwise.
     */
    private static boolean inject(Object object, String route, Bundle routeParametersBundle,
                                  Bundle queryParametersBundle, String metadata) {
        Deeplinking deeplinking = Hoko.deeplinking();
        if (deeplinking != null) {
            Route routeObj = deeplinking.routing().getRoute(route);
            if (routeObj instanceof IntentRouteImpl) {
                IntentRouteImpl hokoIntentRoute = (IntentRouteImpl) routeObj;
                if (routeParametersBundle != null && queryParametersBundle != null) {
                    if (hokoIntentRoute.getRouteParameters() != null) {
                        for (String key : hokoIntentRoute.getRouteParameters().keySet()) {
                            Field field = hokoIntentRoute.getRouteParameters().get(key);
                            String parameter = routeParametersBundle.getString(key);
                            if (parameter == null
                                || !setValueForField(field, object, parameter, true))
                                return false;
                        }
                    }

                    if (hokoIntentRoute.getQueryParameters() != null) {
                        for (String key : hokoIntentRoute.getQueryParameters().keySet()) {
                            Field field = hokoIntentRoute.getQueryParameters().get(key);
                            String parameter = queryParametersBundle.getString(key);
                            if (parameter != null) {
                                setValueForField(field, object, parameter, false);
                            }
                        }
                    }

                    Field metadataField = getMetadataField(object.getClass());
                    if (metadataField != null && metadata != null) {
                        setValueForField(metadataField, object, metadata, false);
                    }

                    return true;
                }
            }
        }
        return false;

    }

    /**
     * Sets values to an object's field independently of access modifiers.
     * It also abstracts the fields actual class or primitive type, performing type-safe casts and
     * conversions.
     *
     * @param field    The field to be set.
     * @param object   The object on which the field should be set
     * @param value    The String value to set on to the field.
     * @param logError true if an error should be logged, false otherwise
     * @return true if the value was properly set, false otherwise.
     */
    private static boolean setValueForField(Field field, Object object, String value,
                                            boolean logError) {
        boolean accessible = field.isAccessible();
        boolean returnValue;
        try {
            field.setAccessible(true);
            Class<?> classObject = field.getType();
            if (field.getType().isPrimitive()) {
                if (classObject.equals(int.class)) {
                    field.setInt(object, Integer.parseInt(value));
                } else if (classObject.equals(float.class)) {
                    field.setFloat(object, Float.parseFloat(value));
                } else if (classObject.equals(double.class)) {
                    field.setDouble(object, Double.parseDouble(value));
                } else if (classObject.equals(short.class)) {
                    field.setShort(object, Short.parseShort(value));
                } else if (classObject.equals(long.class)) {
                    field.setLong(object, Long.parseLong(value));
                } else if (classObject.equals(boolean.class)) {
                    field.setBoolean(object, Boolean.parseBoolean(value));
                } else if (classObject.equals(byte.class)) {
                    field.setByte(object, Byte.parseByte(value));
                } else if (classObject.equals(char.class)) {
                    field.setChar(object, value.charAt(0));
                }
            } else {
                if (classObject.equals(String.class)) {
                    field.set(object, value);
                } else if (classObject.equals(Integer.class)) {
                    field.set(object, Integer.valueOf(value));
                } else if (classObject.equals(Float.class)) {
                    field.set(object, Float.valueOf(value));
                } else if (classObject.equals(Double.class)) {
                    field.set(object, Double.valueOf(value));
                } else if (classObject.equals(Short.class)) {
                    field.set(object, Short.valueOf(value));
                } else if (classObject.equals(Long.class)) {
                    field.set(object, Long.valueOf(value));
                } else if (classObject.equals(Boolean.class)) {
                    field.set(object, Boolean.valueOf(value));
                } else if (classObject.equals(Byte.class)) {
                    field.set(object, Byte.valueOf(value));
                } else if (classObject.equals(Character.class)) {
                    field.set(object, value.charAt(0));
                } else if (classObject.equals(JSONObject.class)) {
                    try {
                        field.set(object, new JSONObject(value));
                    } catch (JSONException e) {
                        if (logError) {
                            HokoLog.e(e);
                        }
                    }
                }
            }
            returnValue = true;
        } catch (IllegalAccessException e) {
            if (logError)
                HokoLog.e(e);
            returnValue = false;
        } finally {
            field.setAccessible(accessible);
        }
        return returnValue;
    }

    /**
     * Returns the String value of a certain field on a certain object, independent of access
     * modifiers.
     * It also provides type-safety by performing casting and conversion from primitive-types and
     * supported classes.
     *
     * @param field  The field where the value will be extracted from.
     * @param object The object on which the field should be extracted.
     * @return The value extracted from the object's field.
     */
    private static String getValueForField(Field field, Object object) {
        boolean accessible = field.isAccessible();
        String returnValue = null;
        try {
            field.setAccessible(true);
            Class<?> classObject = field.getType();
            if (field.getType().isPrimitive()) {
                if (classObject.equals(int.class)) {
                    returnValue = String.valueOf(field.getInt(object));
                } else if (classObject.equals(float.class)) {
                    returnValue = String.valueOf(field.getFloat(object));
                } else if (classObject.equals(double.class)) {
                    returnValue = String.valueOf(field.getDouble(object));
                } else if (classObject.equals(short.class)) {
                    returnValue = String.valueOf(field.getShort(object));
                } else if (classObject.equals(boolean.class)) {
                    returnValue = String.valueOf(field.getBoolean(object));
                } else if (classObject.equals(byte.class)) {
                    returnValue = String.valueOf(field.getByte(object));
                } else if (classObject.equals(char.class)) {
                    returnValue = String.valueOf(field.getChar(object));
                }
            } else {
                if (classObject.equals(String.class)) {
                    returnValue = (String) field.get(object);
                } else if (classObject.equals(Integer.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Float.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Double.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Short.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Boolean.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Byte.class)) {
                    returnValue = field.get(object).toString();
                } else if (classObject.equals(Character.class)) {
                    returnValue = field.get(object).toString();
                }
            }
        } catch (IllegalAccessException e) {
            HokoLog.e(e);
            returnValue = null;
        } finally {
            field.setAccessible(accessible);
        }
        return returnValue;
    }

    /**
     * Check if a certain class is the default route, by checking if it has the DeeplinkDefaultRoute
     * annotation.
     *
     * @param classObject A classObject (usually an activity or a fragment).
     * @return true in case it has the DeeplinkDefaultRoute annotation.
     */
    private static boolean isDefaultRoute(Class classObject) {
        DeeplinkDefaultRoute annotation =
                (DeeplinkDefaultRoute) classObject.getAnnotation(DeeplinkDefaultRoute.class);
        return annotation != null;
    }

    /**
     * This function will parse all activities extracted from the AndroidManifest.xml, retrieving
     * the route format, the activity name, its annotated routeParameters and queryParameters, and
     * finally mapping them to the Deeplinking module according to DeeplinkRoute or
     * DeeplinkDefaultRoute annotations.
     *
     * @param context The application context.
     */
    public static void parseActivities(Context context) {
        List<String> activitiesList = getActivities(context);
        for (String activityName : activitiesList) {
            try {
                Class classObject = Class.forName(activityName);
                mapClassToDeeplink(activityName, classObject, true, true);
            } catch (ClassNotFoundException e) {
                HokoLog.e(e);
            }
        }
    }

    /**
     * This function will parse all the fragment annotations in a given class object and will map
     * those deeplinks to the parent activity.
     *
     * @param classObject  A classObject (usually an activity).
     * @param activityName The activityName.
     */
    private static void parseFragmentActivity(Class classObject, String activityName) {
        DeeplinkFragmentActivity deeplinkFragmentActivityAnnotation =
                getFragmentAnnotationFromClass(classObject);
        if (deeplinkFragmentActivityAnnotation != null) {
            Class[] fragmentClasses = deeplinkFragmentActivityAnnotation.fragments();
            for (Class fragmentClass : fragmentClasses) {
                mapClassToDeeplink(activityName, fragmentClass, false, false);
            }
        }
    }

    /**
     * Maps a given class and activity to a deeplinking route.
     *
     * @param activityName   The activity class name.
     * @param classObject    The class object.
     * @param shouldDefault  true if it should look for a default route, false otherwise.
     * @param shouldFragment true if it should look for fragments inside the class, false otherwise.
     */
    private static void mapClassToDeeplink(String activityName, Class classObject,
                                           boolean shouldDefault, boolean shouldFragment) {
        String route = routeFromClass(classObject);
        Deeplinking deeplinking = Hoko.deeplinking();
        if (deeplinking != null) {
            if (route != null) {
                HashMap<String, Field> routeParameters = getRouteParameters(classObject);
                HashMap<String, Field> queryParameters = getQueryParameters(classObject);
                deeplinking.mapRoute(route, activityName, routeParameters, queryParameters);
            } else {
                mapClassToMultipleDeeplink(activityName, classObject);
            }
            if (shouldDefault && isDefaultRoute(classObject)) {
                HashMap<String, Field> queryParameters = getQueryParameters(classObject);
                deeplinking.mapDefaultRoute(activityName, queryParameters);
            }
        }

        if (shouldFragment) {
            parseFragmentActivity(classObject, activityName);
        }
    }

    private static void mapClassToMultipleDeeplink(String activityName, Class classObject) {
        List<String> routes = routesFromClass(classObject);
        Deeplinking deeplinking = Hoko.deeplinking();
        if (deeplinking != null) {
            if (routes != null) {
                for (String route : routes) {
                    HashMap<String, Field> routeParameters = getRouteParameters(classObject);
                    HashMap<String, Field> queryParameters = getQueryParameters(classObject);
                    deeplinking.mapRoute(route, activityName, routeParameters, queryParameters);
                }
            }
        }
    }

    /**
     * Retrieves the DeeplinkFragmentActivity annotation from a given class.
     *
     * @param classObject The class object.
     * @return The DeeplinkFragmentActivity annotation found, null otherwise.
     */
    private static DeeplinkFragmentActivity getFragmentAnnotationFromClass(Class classObject) {
        return (DeeplinkFragmentActivity) classObject.getAnnotation(DeeplinkFragmentActivity.class);
    }

    /**
     * Finds the fragment which matches to a given route object.
     *
     * @param route   A route string.
     * @param classes An array of Classes.
     * @return The Class that matches the route.
     */
    private static Class findFragmentForRoute(String route, Class[] classes) {
        for (Class classObject : classes) {
            String routeFromClass = routeFromClass(classObject);
            if (routeFromClass != null && routeFromClass.compareTo(route) == 0)
                return classObject;
        }
        return null;
    }

    /**
     * Retrieves the fields annotated with DeeplinkRouteParameter annotation from a given class.
     *
     * @param classObject A classObject (usually an activity).
     * @return A HashMap with the route component as key and the Field as value.
     */
    private static HashMap<String, Field> getRouteParameters(Class classObject) {
        HashMap<String, Field> routeParametersMap = new HashMap<>();
        List<Field> fieldList = getFields(classObject);
        for (Field field : fieldList) {
            DeeplinkRouteParameter routeParameterAnnotation =
                    field.getAnnotation(DeeplinkRouteParameter.class);
            if (routeParameterAnnotation != null) {
                routeParametersMap.put(routeParameterAnnotation.value(), field);
            }
        }
        return routeParametersMap;
    }

    /**
     * Retrieves the fields annotated with DeeplinkQueryParameter annotation from a given class.
     *
     * @param classObject A classObject (usually an activity).
     * @return A HashMap with the query component as key and the Field as value.
     */
    private static HashMap<String, Field> getQueryParameters(Class classObject) {
        HashMap<String, Field> queryParametersMap = new HashMap<>();
        List<Field> fieldList = getFields(classObject);
        for (Field field : fieldList) {
            DeeplinkQueryParameter queryParameterAnnotation =
                    field.getAnnotation(DeeplinkQueryParameter.class);
            if (queryParameterAnnotation != null) {
                queryParametersMap.put(queryParameterAnnotation.value(), field);
            }
        }
        return queryParametersMap;
    }

    private static Field getMetadataField(Class classObject) {
        List<Field> fieldList = getFields(classObject);
        for (Field field : fieldList) {
            DeeplinkMetadata deeplinkMetadataAnnotation = field
                    .getAnnotation(DeeplinkMetadata.class);
            if (deeplinkMetadataAnnotation != null) {
                return field;
            }
        }
        return null;
    }

    /**
     * Retrieves all the fields from a given class.
     *
     * @param classObject A classObject (usually an activity).
     * @return A list of the fields on a given class.
     */
    private static List<Field> getFields(Class classObject) {
        return new ArrayList<>(Arrays.asList(classObject.getDeclaredFields()));
    }

    /**
     * Retrieves all the Activities declared in the AndroidManifest.xml file.
     *
     * @param context The application context.
     * @return A list of the class names for the available activities.
     */
    private static List<String> getActivities(Context context) {
        List<String> activitiesList = new ArrayList<>();
        try {
            ActivityInfo[] activityInfoList = context.getPackageManager()
                    .getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES)
                    .activities;
            for (ActivityInfo activityInfo : activityInfoList) {
                activitiesList.add(activityInfo.name);
            }
        } catch (Exception e) {
            HokoLog.e(e);
        }
        return activitiesList;
    }

}