/*
 * Copyright (C) 2019 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 android.server.wm.intent;

import static androidx.test.InstrumentationRegistry.getInstrumentation;

import android.content.ComponentName;
import android.server.wm.intent.Persistence.LaunchFromIntent;
import android.server.wm.intent.Persistence.LaunchIntent;

import com.google.common.collect.Lists;

import java.util.List;
import java.util.Optional;

/**
 * <pre>
 * The main API entry point for specifying intent tests.
 * A {@code Launch} object is an immutable command object to specify sequences of intents to
 * launch.
 *
 * They can be run by a {@link LaunchRunner}
 * Most tests using this api are defined in {@link Cases}.
 * The two test classes actually using the tests specified there are:
 *
 * 1. {@link IntentGenerationTests}, which runs the cases, records the results and writes them
 * out to device storage
 * 2. {@link IntentTests}, which reads the recorded test files and verifies them again.
 *
 *
 * Features supported by this API are:
 * 1. Starting activities normally or for result.
 * {@code
 *  LaunchSequence.start(intentForResult(RegularActivity.class)) // starting an intent for result.
 *                .append(intent(RegularActivity.class));         // starting an intent normally.
 * }
 * 2. Specifying Intent Flags
 * {@code
 *   LaunchSequence.start(intent(RegularActivity.class).withFlags(Cases.NEW_TASK));
 * }
 *
 * 3. Launching an intent from any point earlier in the launch sequence.
 * {@code
 *   LaunchIntent multipleTask = intent(RegularActivity.class)
 *                                     .withFlags(Cases.NEW_TASK,Cases.MULTIPLE_TASK);
 *   LaunchSequence firstTask = LaunchSequence.start(multipleTask);
 *   firstTask.append(multipleTask).append(intent(SingleTopActivity.class), firstTask);
 * }
 *
 * The above will result in: the first task having two activities in it and the second task having
 * one activity, instead of the normal behaviour adding only one task.
 *
 * It is completely immutable and can therefore be reused without hesitation.
 * Note that making a launch object doesn't start anything yet until it is ran by a
 * {@link LaunchRunner}
 * </pre>
 */
public interface LaunchSequence {
    /**
     * @return the amount of intents that will launch before this {@link LaunchSequence} object.
     */
    int depth();

    /**
     * Extract all the information that has been built up in this {@link LaunchSequence} object, so
     * that {@link LaunchRunner} can run the described sequence of intents.
     */
    LaunchSequenceExecutionInfo fold();

    /**
     * @return the {@link LaunchIntent} inside of this {@link LaunchSequence}.
     */
    LaunchIntent getIntent();

    /**
     * Create a {@link LaunchSequence} object this always has
     * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK} set because without it we can not
     * launch from the application context.
     *
     * @param intent the first intent to be Launched.
     * @return an {@link LaunchSequence} object launching just this intent.
     */
    static LaunchSequence create(LaunchIntent intent) {
        return new RootLaunch(intent.withFlags(Cases.NEW_TASK));
    }

    /**
     * @param intent the next intent that should be launched
     * @return a {@link LaunchSequence} that will launch all the intents in this and then
     * {@code intent}
     */
    default LaunchSequence append(LaunchIntent intent) {
        return new ConsecutiveLaunch(this, intent, false, Optional.empty());
    }

    /**
     * @param intent     the next intent that should be launched
     * @param launchFrom a {@link LaunchSequence} to start the intent from, {@code launchFrom}
     *                   should be a sub sequence of this.
     * @return a {@link LaunchSequence} that will launch all the intents in this and then
     * {@code intent}
     */
    default LaunchSequence append(LaunchIntent intent, LaunchSequence launchFrom) {
        return new ConsecutiveLaunch(this, intent, false, Optional.of(launchFrom));
    }

    /**
     * @param intent the intent to Launch
     * @return a launch with the {@code intent} added to the Act stage.
     */
    default LaunchSequence act(LaunchIntent intent) {
        return new ConsecutiveLaunch(this, intent, true, Optional.empty());
    }

    /**
     * @param intent     the intent to Launch
     * @param launchFrom a {@link LaunchSequence} to start the intent from, {@code launchFrom}
     *                   should be a sub sequence of this.
     * @return a launch with the {@code intent} added to the Act stage.
     */
    default LaunchSequence act(LaunchIntent intent, LaunchSequence launchFrom) {
        return new ConsecutiveLaunch(this, intent, true, Optional.of(launchFrom));
    }

    /**
     * @param activity the activity to create an intent for.
     * @return Creates an {@link LaunchIntent} that will target the {@link android.app.activity}
     * class passed in. see {@link LaunchIntent#withFlags} to add intent flags to the returned
     * intent.
     */
    static LaunchIntent intent(Class<? extends android.app.Activity> activity) {
        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, false);
    }


    /**
     * Creates an {@link LaunchIntent} that will be started with {@link
     * android.app.Activity#startActivityForResult}
     *
     * @param activity the activity to create an intent for.
     */
    static LaunchIntent intentForResult(Class<? extends android.app.Activity> activity) {
        return new LaunchIntent(Lists.newArrayList(), createComponent(activity), null, true);
    }

    String packageName = getInstrumentation().getTargetContext().getPackageName();

    static ComponentName createComponent(Class<? extends android.app.Activity> activity) {
        return new ComponentName(packageName, activity.getName());
    }

    /**
     * Allows {@link LaunchSequence} objects to form a linkedlist of intents to launch.
     */
    class ConsecutiveLaunch implements LaunchSequence {
        private final LaunchSequence mPrevious;
        private final LaunchIntent mLaunchIntent;
        private final boolean mAct;

        public ConsecutiveLaunch(LaunchSequence previous, LaunchIntent launchIntent,
                boolean act, Optional<LaunchSequence> launchFrom) {
            mPrevious = previous;
            mLaunchIntent = launchIntent;
            mAct = act;
            mLaunchFrom = launchFrom;
        }

        /**
         * This should always be a {@link LaunchSequence} that is in mPrevious.
         */
        @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
        private Optional<LaunchSequence> mLaunchFrom;

        @Override
        public int depth() {
            return mPrevious.depth() + 1;
        }

        @Override
        public LaunchSequenceExecutionInfo fold() {
            LaunchSequenceExecutionInfo launchSequenceExecutionInfo = mPrevious.fold();
            int launchSite = mLaunchFrom.map(LaunchSequence::depth).orElse(this.depth() - 1);

            LaunchFromIntent intent = new LaunchFromIntent(mLaunchIntent, launchSite);
            if (mAct) {
                launchSequenceExecutionInfo.acts.add(intent);
            } else {
                launchSequenceExecutionInfo.setup.add(intent);
            }
            return launchSequenceExecutionInfo;
        }

        @Override
        public LaunchIntent getIntent() {
            return mLaunchIntent;
        }
    }

    /**
     * The first intent to launch in a {@link LaunchSequence} Object.
     */
    class RootLaunch implements LaunchSequence {
        /**
         * The intent that should be launched.
         */
        private final LaunchIntent mLaunchIntent;

        public RootLaunch(LaunchIntent launchIntent) {
            mLaunchIntent = launchIntent;
        }

        @Override
        public int depth() {
            return 0;
        }

        @Override
        public LaunchSequenceExecutionInfo fold() {
            return new LaunchSequenceExecutionInfo(
                    Lists.newArrayList(new LaunchFromIntent(mLaunchIntent, -1)),
                    Lists.newArrayList());
        }

        @Override
        public LaunchIntent getIntent() {
            return mLaunchIntent;
        }
    }

    /**
     * Information for the {@link LaunchRunner} to run the launch represented by a {@link
     * LaunchSequence} object. The {@link LaunchSequence} object is "folded" as a list
     * into the summary of all intents with the corresponding indexes from what context
     * to launch them.
     */
    class LaunchSequenceExecutionInfo {
        List<LaunchFromIntent> setup;
        List<LaunchFromIntent> acts;

        public LaunchSequenceExecutionInfo(List<LaunchFromIntent> setup,
                List<LaunchFromIntent> acts) {
            this.setup = setup;
            this.acts = acts;
        }
    }
}

