// import { navigateScreen } from './ApplicationRoute.js'
export var ActivityLifecycle;
(function (ActivityLifecycle) {
    /**
     * The activity is initialized, but not yet created.
     * This is the initial state of the activity.
     */
    ActivityLifecycle[ActivityLifecycle["Initialized"] = 0] = "Initialized";
    /**
     * The activity is created, but not yet started.
     * This is the state after the `onCreate()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Created"] = 1] = "Created";
    /**
     * The activity is started, but not yet resumed.
     * This is the state after the `onStart()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Started"] = 2] = "Started";
    /**
     * The activity is resumed and visible to the user.
     * This is the state after the `onResume()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Resumed"] = 3] = "Resumed";
    /**
     * The activity is paused, but still visible to the user.
     * This is the state after the `onPause()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Paused"] = 4] = "Paused";
    /**
     * The activity is stopped and no longer visible to the user.
     * This is the state after the `onStop()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Stopped"] = 5] = "Stopped";
    /**
     * The activity is destroyed and no longer exists.
     * This is the state after the `onDestroy()` method is called.
     */
    ActivityLifecycle[ActivityLifecycle["Destroyed"] = 6] = "Destroyed";
})(ActivityLifecycle || (ActivityLifecycle = {}));
export var IntentFlags;
(function (IntentFlags) {
    /**
     * Marks that the activity should be rendered as a modal.
     */
    IntentFlags[IntentFlags["Modal"] = 1] = "Modal";
    /**
     * The activity is started for result.
     */
    IntentFlags[IntentFlags["ForResult"] = 2] = "ForResult";
    /**
     * When set, the intent data is passed by reference.
     * By default the activity manager makes a copy of the intent data.
     * This is useful when the data is large and you want to avoid copying it
     * or when you want to share the same data between activities.
     */
    IntentFlags[IntentFlags["ByReference"] = 4] = "ByReference";
})(IntentFlags || (IntentFlags = {}));
export var IntentResult;
(function (IntentResult) {
    IntentResult[IntentResult["RESULT_CANCELED"] = 0] = "RESULT_CANCELED";
    IntentResult[IntentResult["RESULT_OK"] = 1] = "RESULT_OK";
})(IntentResult || (IntentResult = {}));
/**
 * Manages application activities.
 *
 * An activity is a screen or a portion of a rendered screen that has its own
 * lifecycle methods and routing.
 *
 * An activity is registered by its class definition. Because of that, activities
 * cannot have constructors that require own parameters. Activities have to be
 * self contained or use the event system to request data.
 */
export class ActivityManager {
    currentActivity;
    /**
     * Stores activity *classes* by unique ID (action name).
     */
    activityClasses = new Map();
    /**
     * Stores the resolvers for activities started for result.
     */
    resultResolvers = new Map();
    /**
     * Represents the activity stack. It is used to manage
     * the UI state after calling the back action.
     */
    activityStack = [];
    /**
     * A stack of activities that are rendered over other activities
     * in a dialog.
     */
    modalActivityStack = [];
    generateActivityId() {
        return Math.random().toString(36).substring(2, 15);
    }
    #parent;
    constructor(parent) {
        this.#parent = parent;
        // this.reflectEvent = this.reflectEvent.bind(this)
    }
    /**
     * Registers an activity class with enhanced options.
     * @param action The action name to register the activity for.
     * @param activityClass The activity class constructor.
     * @param options Optional registration configuration.
     */
    registerActivity(action, activityClass, options = {}) {
        const { override = false } = options;
        if (this.activityClasses.has(action) && !override) {
            // eslint-disable-next-line no-console
            console.warn(`Activity with action "${action}" already registered.`, new Error().stack);
            return;
        }
        // Validate that the class extends Activity
        if (typeof activityClass !== 'function') {
            throw new Error(`Invalid activity class for action "${action}": must be a constructor function`);
        }
        this.activityClasses.set(action, activityClass);
    }
    /**
     * Creates an activity and pushes it into the stack, but it does not initialize any of the lifecycle methods.
     * It is a way to put an activity at some place of the stack,
     * but it will be only initialized when other activities finis.
     *
     * Note, if you add an uninitialized activity and you won't add more activities then this activity may never run.
     * Make sure that there's always another activity.
     *
     * @param intent The intent that created this activity.
     */
    async createActivity(intent) {
        const activity = this.buildActivity(intent);
        const activityId = this.generateActivityId();
        const stackEntry = { activity, id: activityId, intent, action: intent.action };
        this.activityStack.push(stackEntry);
    }
    // setupRoute(intent: Intent): void {
    //   if (intent.data) {
    //     const typed = intent.data as { uri?: string }
    //     if (typed.uri) {
    //       navigateScreen(typed.uri)
    //     }
    //   }
    // }
    /**
     * Starts a new activity and brings it to the foreground.
     *
     * @param intent The intent that created this activity.
     */
    async startActivity(intent) {
        const { currentActivity, activityStack } = this;
        const lastIndex = activityStack.findLastIndex((entry) => entry.action === intent.action);
        // if the activity is in the history list, we bring it up and update intent.
        if (lastIndex > -1) {
            const info = activityStack[lastIndex];
            activityStack.splice(lastIndex, 1);
            activityStack.push({
                id: info.id,
                activity: info.activity,
                action: info.action,
                intent,
            });
            await info.activity.onNewIntent(intent);
            // TODO: Check if the activity is destroyed.
            // this.setupRoute(intent)
            await this.updateCurrentActivity(info.activity, false);
            return info.activity;
        }
        const activity = this.buildActivity(intent);
        if (currentActivity) {
            await currentActivity.onPause();
            currentActivity.lifecycle = ActivityLifecycle.Paused;
        }
        const activityId = this.generateActivityId();
        const stackEntry = { activity, id: activityId, intent, action: intent.action };
        // Modal activities are pretty much the same as regular activities but are rendered on top
        // of other activities. We keep track of them on another stack as all modal activities must
        // finish before returning to the regular stack.
        const isModal = (intent.flags ?? 0) & IntentFlags.Modal;
        if (isModal) {
            this.modalActivityStack.push(stackEntry);
        }
        else {
            this.activityStack.push(stackEntry);
        }
        await activity.onCreate(intent);
        if (this.isDestroyed(activity)) {
            // the activity finished and the manager already took care of this situation.
            return activity;
        }
        activity.lifecycle = ActivityLifecycle.Created;
        await this.updateCurrentActivity(activity, !!isModal);
        if (this.isDestroyed(activity)) {
            // the activity finished and the manager already took care of this situation.
            return activity;
        }
        // this.setupRoute(intent)
        return activity;
    }
    buildActivity(intent) {
        const action = intent.action;
        if (!this.activityClasses.has(action)) {
            throw new Error(`No activity class registered for action: ${action}`);
        }
        const ActivityClass = this.activityClasses.get(action);
        if (!ActivityClass) {
            throw new Error(`No activity class registered for action: ${action}`);
        }
        // We use the container to resolve dependencies, if any.
        // const activity = (await container.make(ActivityClass, [this.#parent])) as Activity
        return new ActivityClass(this.#parent);
    }
    /**
     * Starts an activity that should return a result to the calling activity.
     *
     * @template T The type of the result data expected.
     * @param intent The intent to start the activity.
     * @returns A promise that resolves when the started activity finishes, returning the result Intent with
     * data of type T.
     * @example
     * ```typescript
     * const resultIntent = await mgr.startActivityForResult<{ userId: string }>({ action: 'pickUser' });
     * if (resultIntent.resultCode === IntentResult.RESULT_OK) {
     *   console.log(resultIntent.data?.userId);
     * }
     * ```
     */
    async startActivityForResult(intent) {
        const byReference = (intent.flags ?? 0) & IntentFlags.ByReference;
        const shallowCopy = { ...intent };
        const data = shallowCopy.data;
        if (byReference) {
            delete shallowCopy.data;
        }
        const deepCopy = structuredClone(shallowCopy);
        if (data && byReference) {
            deepCopy.data = data;
        }
        if (deepCopy.flags) {
            deepCopy.flags = deepCopy.flags | IntentFlags.ForResult;
        }
        else {
            deepCopy.flags = IntentFlags.ForResult;
        }
        return new Promise((resolve, reject) => {
            this.resultResolvers.set(deepCopy, resolve);
            this.startActivity(deepCopy).catch((e) => {
                this.resultResolvers.delete(deepCopy);
                reject(e);
            });
        });
    }
    isDestroyed(activity) {
        return activity.lifecycle === ActivityLifecycle.Destroyed;
    }
    /**
     * @param activity The activity to finish.
     */
    async finishActivity(activity) {
        this.setupStyles(undefined, activity);
        const isModal = this.modalActivityStack.some((entry) => entry.activity === activity);
        const stack = isModal ? this.modalActivityStack : this.activityStack;
        const index = stack.findIndex((entry) => entry.activity === activity);
        if (index !== -1) {
            const stackEntry = stack.splice(index, 1)[0];
            await stackEntry.activity.onPause();
            stackEntry.activity.lifecycle = ActivityLifecycle.Paused;
            await stackEntry.activity.onStop();
            stackEntry.activity.lifecycle = ActivityLifecycle.Stopped;
            await stackEntry.activity.onDestroy();
            stackEntry.activity.lifecycle = ActivityLifecycle.Destroyed;
            // Clean up resources to prevent memory leaks
            this.cleanupActivity(stackEntry.activity);
            this.resolveActivityResult(stackEntry.activity, stackEntry.intent);
            // Resume the previous activity
            await this.bringLastActivityToFront();
        }
        else {
            if (activity.lifecycle === ActivityLifecycle.Created) {
                await activity.onStop();
                activity.lifecycle = ActivityLifecycle.Stopped;
            }
            await activity.onDestroy();
            activity.lifecycle = ActivityLifecycle.Destroyed;
            this.resolveActivityResult(activity, { action: '' });
            // This can happen when an activity finishes in one of the callback methods,
            // before it is added to the stack. In that case, we bring the last activity back to the front.
            await this.bringLastActivityToFront();
        }
        this.#parent.requestUpdate();
    }
    resolveActivityResult(activity, originalIntent) {
        const resolver = this.resultResolvers.get(originalIntent);
        // The activity might have not been started with startActivityForResult.
        // A thing to consider for the future is whether we should throw an error in a case where
        // the activity was started for result but the resolver was not found.
        if (resolver) {
            this.resultResolvers.delete(originalIntent);
            // We construct the result intent.
            // Ideally we would like to preserve the original intent structure but with new data.
            const resultIntent = {
                ...originalIntent,
                data: activity.getResult(),
                result: activity.resultCode,
            };
            resolver(resultIntent);
        }
    }
    async bringLastActivityToFront() {
        if (this.modalActivityStack.length > 0) {
            const previousEntry = this.modalActivityStack[this.activityStack.length - 1];
            await this.updateCurrentActivity(previousEntry.activity, false, previousEntry.intent);
        }
        else if (this.activityStack.length > 0) {
            const previousEntry = this.activityStack[this.activityStack.length - 1];
            await this.updateCurrentActivity(previousEntry.activity, false, previousEntry.intent);
        }
        else {
            this.currentActivity = undefined; // No more activities
        }
    }
    getTopActivity() {
        const topModal = this.modalActivityStack.findLast((a) => a.activity.lifecycle === ActivityLifecycle.Resumed);
        if (topModal) {
            return topModal.activity;
        }
        const top = this.activityStack.findLast((a) => a.activity.lifecycle === ActivityLifecycle.Resumed);
        return top ? top.activity : undefined;
    }
    getCurrentActivity() {
        return this.currentActivity;
    }
    /**
     * Sets the styles for the current activity.
     * This method adds or removes CSS classes based on the activity's class name
     * and action, allowing for dynamic styling based on the active activity.
     * @param activity The activity to set styles for.
     * If not provided, it will not set any styles.
     * If the activity is the same as the current one, it does nothing.
     */
    setupStyles(activity, oldActivity) {
        if (activity === oldActivity) {
            return;
        }
        const root = document?.documentElement;
        if (!root) {
            return;
        }
        if (oldActivity) {
            const ctor = oldActivity.constructor;
            root.classList.remove(ctor.name.toLowerCase());
            if (ctor.action) {
                root.classList.remove(ctor.action.replace(/\//g, '-'));
            }
        }
        if (activity) {
            const ctor = activity.constructor;
            root.classList.add(ctor.name.toLowerCase());
            if (ctor.action) {
                root.classList.add(ctor.action.replace(/\//g, '-'));
            }
        }
    }
    /**
     * A helper method to handle the transition between activities, ensuring
     * lifecycle methods are called in the correct order (pause the previous activity,
     * then start/resume the new one). This centralizes lifecycle management.
     *
     * @param activity The activity to bring to the foreground.
     */
    async updateCurrentActivity(activity, isModal, intent) {
        this.setupStyles(activity, this.currentActivity);
        if (this.currentActivity &&
            this.currentActivity !== activity &&
            this.currentActivity.lifecycle !== ActivityLifecycle.Destroyed &&
            // Make sure we don't pause the current activity if it's already paused.
            this.currentActivity.lifecycle !== ActivityLifecycle.Paused) {
            await this.currentActivity.onPause();
            this.currentActivity.lifecycle = ActivityLifecycle.Paused;
        }
        switch (activity.lifecycle) {
            case ActivityLifecycle.Paused:
                if (!(await this.transitionStartedToResumed(activity))) {
                    return;
                }
                break;
            case ActivityLifecycle.Stopped:
                await this.safeExecuteLifecycleMethod(() => activity.onRestart(), activity, 'onRestart');
                activity.lifecycle = ActivityLifecycle.Resumed;
                break;
            case ActivityLifecycle.Destroyed:
                throw new Error(`Invalid state. The activity is already destroyed.`);
            case ActivityLifecycle.Created:
                if (!(await this.transitionCreatedToResumed(activity))) {
                    return;
                }
                break;
            case ActivityLifecycle.Started:
                if (!(await this.transitionStartedToResumed(activity))) {
                    return;
                }
                break;
            case ActivityLifecycle.Initialized:
                if (!(await this.transitionInitializedToResumed(activity, intent))) {
                    return;
                }
                break;
        }
        if (!isModal) {
            // With a modal activity, it renders directly to the `<body>` and renders outside
            // of the normal rendering flow. We keep the previous activity rendered.
            this.currentActivity = activity;
        }
    }
    /**
     * Safely executes an async lifecycle method with error handling.
     * @param lifecycleMethod The lifecycle method to execute.
     * @param activity The activity instance.
     * @param methodName The name of the method for error reporting.
     * @returns Promise that resolves when the method completes or rejects with wrapped error.
     */
    async safeExecuteLifecycleMethod(lifecycleMethod, activity, methodName) {
        try {
            await lifecycleMethod();
        }
        catch (error) {
            const activityName = activity.constructor.name;
            const wrappedError = new Error(`Error in ${activityName}.${methodName}(): ${error instanceof Error ? error.message : String(error)}`);
            wrappedError.cause = error;
            // Allow the activity to handle its own errors, but still propagate
            this.#parent.dispatchEvent(new CustomEvent('activity-lifecycle-error', {
                detail: { activity, methodName, error: wrappedError },
            }));
            throw wrappedError;
        }
    }
    /**
     * Allows navigating back through the activity stack.
     * Call `ActivityManager.navigateBack()` when the user performs a back action
     * (e.g., clicks a back button).
     * @returns Promise that resolves to true if navigation was successful, false if at root.
     */
    async navigateBack() {
        // Handle modal activities first
        if (this.modalActivityStack.length > 0) {
            const topModal = this.modalActivityStack[this.modalActivityStack.length - 1];
            await this.finishActivity(topModal.activity);
            return true;
        }
        // Handle regular activities
        if (this.activityStack.length > 1 && this.currentActivity) {
            await this.finishActivity(this.currentActivity);
            return true;
        }
        // At root, cannot navigate back
        return false;
    }
    /**
     * Navigates to a specific activity in the stack, clearing activities above it.
     * @param action The action of the activity to navigate to.
     * @returns Promise that resolves to true if navigation was successful.
     */
    async navigateToActivity(action) {
        const index = this.activityStack.findIndex((entry) => entry.action === action);
        if (index === -1) {
            return false;
        }
        // Finish all activities above the target
        const activitiesToFinish = this.activityStack.slice(index + 1);
        for (const entry of activitiesToFinish) {
            await this.finishActivity(entry.activity);
        }
        // Bring the target activity to the front
        const targetEntry = this.activityStack[index];
        await this.updateCurrentActivity(targetEntry.activity, false, targetEntry.intent);
        return true;
    }
    /**
     * Clears the entire activity stack and starts a new root activity.
     * @param intent The intent for the new root activity.
     */
    async clearStackAndStart(intent) {
        // Finish all activities
        const allActivities = [...this.activityStack, ...this.modalActivityStack];
        for (const entry of allActivities) {
            if (entry.activity.lifecycle !== ActivityLifecycle.Destroyed) {
                await this.finishActivity(entry.activity);
            }
        }
        // Clear stacks
        this.activityStack.length = 0;
        this.modalActivityStack.length = 0;
        this.currentActivity = undefined;
        // Start new root activity
        await this.startActivity(intent);
    }
    /**
     * Creates an intent with better type safety and validation.
     * @param action The action name.
     * @param data Optional intent data.
     * @param options Optional intent configuration.
     * @returns A properly formed intent.
     */
    createIntent(action, data, options = {}) {
        if (!this.isActionRegistered(action)) {
            throw new Error(`Cannot create intent for unregistered action: ${action}`);
        }
        return {
            action,
            data,
            ...options,
        };
    }
    createRequestCode() {
        const min = 1000;
        const max = 9999;
        return Math.floor(Math.random() * (max - min) + min);
    }
    findActiveActivity(action) {
        return this.activityStack.find((entry) => entry.action === action)?.activity;
    }
    /**
     * Transitions an activity from Created to Resumed state.
     * @param activity The activity to transition.
     * @returns true if successful, false if activity was destroyed during transition.
     */
    async transitionCreatedToResumed(activity) {
        await this.safeExecuteLifecycleMethod(() => activity.onStart(), activity, 'onStart');
        if (this.isDestroyed(activity)) {
            return false;
        }
        activity.lifecycle = ActivityLifecycle.Started;
        await this.safeExecuteLifecycleMethod(() => activity.onResume(), activity, 'onResume');
        if (this.isDestroyed(activity)) {
            return false;
        }
        activity.lifecycle = ActivityLifecycle.Resumed;
        activity.requestUpdate();
        return true;
    }
    /**
     * Transitions an activity from Started to Resumed state.
     * @param activity The activity to transition.
     * @returns true if successful, false if activity was destroyed during transition.
     */
    async transitionStartedToResumed(activity) {
        await this.safeExecuteLifecycleMethod(() => activity.onResume(), activity, 'onResume');
        if (this.isDestroyed(activity)) {
            return false;
        }
        activity.lifecycle = ActivityLifecycle.Resumed;
        activity.requestUpdate();
        return true;
    }
    /**
     * Transitions an activity from Initialized to Resumed state.
     * @param activity The activity to transition.
     * @param intent The intent used to create the activity.
     * @returns true if successful, false if activity was destroyed during transition.
     */
    async transitionInitializedToResumed(activity, intent) {
        await this.safeExecuteLifecycleMethod(() => activity.onCreate(intent), activity, 'onCreate');
        activity.lifecycle = ActivityLifecycle.Created;
        if (this.isDestroyed(activity)) {
            return false;
        }
        return await this.transitionCreatedToResumed(activity);
    }
    /**
     * Cleans up resources for destroyed activities to prevent memory leaks.
     * @param activity The activity to clean up.
     */
    cleanupActivity(activity) {
        // Remove any event listeners that might create memory leaks
        // This changes the entire prototype chain of the activity,
        // effectively making it a new object. We can't do that as the activity will loose some
        // of its properties, so we don't do it for now.
        // if (activity instanceof EventTarget) {
        //   // Clear all event listeners by replacing the activity with a new EventTarget
        //   // This is a defensive approach since we can't enumerate listeners
        //   Object.setPrototypeOf(activity, EventTarget.prototype)
        // }
        // Clear any references that might prevent garbage collection
        activity.renderRoot = undefined;
    }
    /**
     * Validates that an activity stack is in a consistent state.
     * @param stack The activity stack to validate.
     * @returns Array of validation errors, empty if valid.
     */
    validateStackConsistency(stack) {
        const errors = [];
        const seenIds = new Set();
        for (const entry of stack) {
            if (seenIds.has(entry.id)) {
                errors.push(`Duplicate activity ID found: ${entry.id}`);
            }
            seenIds.add(entry.id);
            if (!entry.activity || !entry.id || !entry.action) {
                errors.push(`Invalid stack entry: missing required properties`);
            }
            if (entry.activity.lifecycle === ActivityLifecycle.Destroyed) {
                errors.push(`Destroyed activity found in stack: ${entry.id}`);
            }
        }
        return errors;
    }
    /**
     * Gets diagnostic information about the current state of the activity manager.
     * Useful for debugging and monitoring.
     * @returns Object containing current state information.
     */
    getDiagnostics() {
        return {
            totalActivities: this.activityStack.length,
            modalActivities: this.modalActivityStack.length,
            currentActivity: this.currentActivity?.constructor.name,
            topActivity: this.getTopActivity()?.constructor.name,
            stackValidation: this.validateStackConsistency(this.activityStack),
            modalStackValidation: this.validateStackConsistency(this.modalActivityStack),
        };
    }
    /**
     * Gets all registered activity actions.
     * @returns Array of registered action names.
     */
    getRegisteredActions() {
        return Array.from(this.activityClasses.keys());
    }
    /**
     * Checks if an action is registered.
     * @param action The action to check.
     * @returns true if the action is registered, false otherwise.
     */
    isActionRegistered(action) {
        return this.activityClasses.has(action);
    }
}
//# sourceMappingURL=ActivityManager.js.map