/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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 com.example.android.intentplayground;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * Static utility functions to query intent and activity manifest flags.
 */
class FlagUtils {
    private static Class<Intent> sIntentClass = Intent.class;
    private static List<ActivityInfo> sActivityInfos = null;
    private static Intent sIntent = new Intent();
    static final String INTENT_FLAG_PREFIX = "FLAG_ACTIVITY";
    private static final String ACTIVITY_INFO_FLAG_PREFIX = "FLAG";
    
    /**
     * Returns a String list of flags active on this intent.
     * @param intent The intent on which to query flags.
     * @return A list of flags active on this intent.
     */
    public static List<String> discoverFlags(Intent intent) {
        int flags = intent.getFlags();
        return Arrays.stream(intent.getClass().getDeclaredFields()) // iterate over Intent members
                .filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX)) // filter FLAG_ fields
                .filter(f -> {
                    try {
                        return (flags & f.getInt(intent)) > 0;
                    } catch (IllegalAccessException e) {
                        // Should never happen, the fields we are reading are public
                        throw new RuntimeException(e);
                    }
                })  // filter fields that are present in intent
                .map(Field::getName) // map present Fields to their string names
                .collect(Collectors.toList());
    }

    /**
     * Returns a full list of flags available to be set on an intent.
     * @return A string list of all intent flags.
     */
    public static List<String> getIntentFlagsAsString() {
        return Arrays.stream(sIntentClass.getDeclaredFields())
                .filter(f -> f.getName().startsWith(INTENT_FLAG_PREFIX))
                .map(Field::getName)
                .collect(Collectors.toList());
    }

    /**
     * Get all defined {@link IntentFlag}s.
     * @return All defined IntentFlags.
     */
    public static List<IntentFlag> getAllIntentFlags() {
        return Arrays.asList(IntentFlag.values());
    }

    /**
     * Get intent flags by category/
     * @return List of string flags (value) organized by category/function (key).
     */
    public static Map<String, List<String>> intentFlagsByCategory() {
        Map<String, List<String>> categories = new HashMap<>();
        List<String> allFlags = getIntentFlagsAsString();
        List<String> nonUser = new LinkedList<>();
        List<String> recentsAndUi = new LinkedList<>();
        List<String> newTask = new LinkedList<>();
        List<String> clearTask = new LinkedList<>();
        List<String> rearrangeTask = new LinkedList<>();
        List<String> other = new LinkedList<>();
        allFlags.forEach(flag -> {
            if (hasSuffix(flag, "BROUGHT_TO_FRONT", "LAUNCHED_FROM_HISTORY")) {
                nonUser.add(flag);
            } else if (hasSuffix(flag, "RECENTS", "LAUNCH_ADJACENT", "NO_ANIMATION", "NO_HISTORY",
                    "RETAIN_IN_RECENTS")) {
                recentsAndUi.add(flag);
            } else if (hasSuffix(flag, "MULTIPLE_TASK", "NEW_TASK", "NEW_DOCUMENT",
                    "RESET_TASK_IF_NEEDED")) {
                newTask.add(flag);
            } else if (hasSuffix(flag, "CLEAR_TASK", "CLEAR_TOP", "CLEAR_WHEN_TASK_RESET")) {
                clearTask.add(flag);
            } else if (hasSuffix(flag, "REORDER_TO_FRONT", "SINGLE_TOP", "TASK_ON_HOME")) {
                rearrangeTask.add(flag);
            } else {
                other.add(flag);
            }
        });
        categories.put("Non-user", nonUser);
        categories.put("Recents and UI", recentsAndUi);
        categories.put("New Task", newTask);
        categories.put("Clear Task", clearTask);
        categories.put("Rearrange Task", rearrangeTask);
        categories.put("Other", other);
        return categories;
    }

    /**
     * Checks the target string for any of the listed suffixes.
     * @param target The string to test for suffixes.
     * @param suffixes The suffixes to test the string for.
     * @return True if the target string has any of the suffixes, false if not.
     */
    private static boolean hasSuffix(String target, String... suffixes) {
        for (String suffix: suffixes) {
            if (target.endsWith(suffix)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets the integer value of an intent flag.
     * @param flagName The field name of the flag.
     */
    public static int flagValue(String flagName)  {
        try {
            return sIntentClass.getField(flagName).getInt(sIntent);
        } catch (Exception e) {
            return 0;
        }
    }

    /**
     * Checks if the passed intent has the specified flag.
     * @param intent The intent of which to examine the flags.
     * @param flagName The string name of the intent flag to check for.
     * @return True if the flag is present, false if not.
     */
    public static boolean hasIntentFlag(Intent intent, String flagName) {
        return (intent.getFlags() & flagValue(flagName)) > 0;
    }

    /**
     * Checks if the passed intent has the specified flag.
     * @param intent The intent of which to examine the flags.
     * @param flag The corresponding enum {@link IntentFlag} of the intent flag to check for.
     * @return True if the flag is present, false if not.
     */
    public static boolean hasIntentFlag(Intent intent, IntentFlag flag) {
        return hasIntentFlag(intent, flag.getName());
    }

    /**
     * Checks if the passed activity has the specified flag set in its manifest.
     * @param context A context from this application (used to access {@link PackageManager}.
     * @param activity The activity of which to examine the flags.
     * @param flag The corresponding enum {@link ActivityFlag} of the activity flag to check for.
     * @return True if the flag is present, false if not.
     */
    public static boolean hasActivityFlag(Context context, ComponentName activity,
                                          ActivityFlag flag) {
        return getActivityFlags(context, activity).contains(flag);
    }

    /**
     * Returns an {@link EnumSet} of {@link ActivityFlag} corresponding to activity manifest flags
     * activity on the specified activity.
     * @param context A context from this application (used to access {@link PackageManager}.
     * @param activity The activity of which to examine the flags.
     * @return  A set of ActivityFlags corresponding to activity manifest flags.
     */
    public static EnumSet<ActivityFlag> getActivityFlags(Context context, ComponentName activity) {
        loadActivityInfo(context);
        EnumSet<ActivityFlag> flags = EnumSet.noneOf(ActivityFlag.class);
        Optional<ActivityInfo> infoOptional = sActivityInfos.stream()
                .filter(i-> i.name.equals(activity.getClassName()))
                .findFirst();
        if (!infoOptional.isPresent()) {
            return flags;
        }
        ActivityInfo info = infoOptional.get();
        if ((info.flags & ActivityInfo.FLAG_CLEAR_TASK_ON_LAUNCH) > 0) {
            flags.add(ActivityFlag.CLEAR_TASK_ON_LAUNCH);
        }
        if ((info.flags & ActivityInfo.FLAG_ALLOW_TASK_REPARENTING) > 0) {
            flags.add(ActivityFlag.ALLOW_TASK_REPARENTING);
        }
        switch (info.launchMode) {
            case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
                flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_INSTANCE);
                break;
            case ActivityInfo.LAUNCH_SINGLE_TASK:
                flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TASK);
                break;
            case ActivityInfo.LAUNCH_SINGLE_TOP:
                flags.add(ActivityFlag.LAUNCH_MODE_SINGLE_TOP);
                break;
            case ActivityInfo.LAUNCH_MULTIPLE:
            default:
                flags.add(ActivityFlag.LAUNCH_MODE_STANDARD);
                break;
        }
        switch(info.documentLaunchMode) {
            case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
                flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_INTO_EXISTING);
                break;
            case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
                flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_ALWAYS);
                break;
            case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
                flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NEVER);
                break;
            case ActivityInfo.DOCUMENT_LAUNCH_NONE:
            default:
                flags.add(ActivityFlag.DOCUMENT_LAUNCH_MODE_NONE);
                break;
        }
        return flags;
    }

    private static void loadActivityInfo(Context context) {
        if (sActivityInfos == null) {
            PackageInfo packInfo;

            // Retrieve activities and their manifest flags
            PackageManager pm = context.getPackageManager();
            try {
                packInfo = pm.getPackageInfo(context.getPackageName(),
                        PackageManager.GET_ACTIVITIES);
            } catch (PackageManager.NameNotFoundException e) {
                throw new RuntimeException(e);
            }
            sActivityInfos = Arrays.asList(packInfo.activities);
        }
    }

    /**
     * Discover which flags on the specified {@link ActivityInfo} are enabled,
     * and return them as a list of strings.
     * @param activity The activity from which you want to find flags.
     * @return A list of flags.
     */
    public static List<String> getActivityFlags(ActivityInfo activity) {
        int flags = activity.flags;
        List<String> flagStrings = Arrays.stream(activity.getClass().getDeclaredFields())
                .filter(f -> f.getName().startsWith(ACTIVITY_INFO_FLAG_PREFIX))
                .filter(f -> {
                    try {
                        return (flags & f.getInt(activity)) > 0;
                    } catch (IllegalAccessException e) {
                        // Should never happen, the fields we are reading are public
                        throw new RuntimeException(e);
                    }
                })  // filter fields that are present in intent
                .map(Field::getName) // map present Fields to their string names
                .map(name -> camelify(name.substring(ACTIVITY_INFO_FLAG_PREFIX.length())))
                .map(s -> s.concat("=true"))
                .collect(Collectors.toList());
        // check for launchMode
        if (activity.launchMode != 0) {
            String lm = "launchMode=";
            switch(activity.launchMode) {
                case ActivityInfo.LAUNCH_SINGLE_INSTANCE:
                    lm += "singleInstance";
                    break;
                case ActivityInfo.LAUNCH_SINGLE_TASK:
                    lm += "singleTask";
                    break;
                case ActivityInfo.LAUNCH_SINGLE_TOP:
                    lm += "singleTop";
                    break;
                case ActivityInfo.LAUNCH_SINGLE_INSTANCE_PER_TASK:
                    lm += "singleInstancePerTask";
                    break;
                case ActivityInfo.LAUNCH_MULTIPLE:
                default:
                    lm += "standard";
                    break;
            }
            flagStrings.add(lm);
        }
        // check for documentLaunchMode
        if (activity.documentLaunchMode != 0) {
            String dlm = "documentLaunchMode=";
            switch(activity.documentLaunchMode) {
                case ActivityInfo.DOCUMENT_LAUNCH_INTO_EXISTING:
                    dlm += "intoExisting";
                    break;
                case ActivityInfo.DOCUMENT_LAUNCH_ALWAYS:
                    dlm += "always";
                    break;
                case ActivityInfo.DOCUMENT_LAUNCH_NEVER:
                    dlm += "never";
                    break;
                case ActivityInfo.DOCUMENT_LAUNCH_NONE:
                default:
                    dlm += "none";
                    break;
            }
            flagStrings.add(dlm);
        }
        if (activity.taskAffinity != null) {
            flagStrings.add("taskAffinity="+ activity.taskAffinity);
        }
        return flagStrings;
    }

    /**
     * Takes a snake_case and converts to CamelCase.
     * @param snake A snake_case string.
     * @return A camelified string.
     */
    public static String camelify(String snake) {
        List<String> words = Arrays.asList(snake.split("_"));
        StringBuilder output = new StringBuilder(words.get(0).toLowerCase());
        words.subList(1,words.size()).forEach(s -> {
            String first = s.substring(0,1).toUpperCase();
            String rest = s.substring(1).toLowerCase();
            output.append(first).append(rest);
        });
        return output.toString();
    }

    /**
     * Retrieves the corresponding enum {@link IntentFlag} for the string flag.
     * @param stringFlag the name of the intent flag.
     * @return The corresponding IntentFlag.
     */
    public static IntentFlag getFlagForString(String stringFlag) {
        return getAllIntentFlags().stream().filter(flag -> flag.getName().equals(stringFlag)).findAny()
                .orElse(null);
    }
}

