import { __esDecorate, __runInitializers } from "tslib";
import { nothing } from 'lit';
import { FragmentState, FragmentManager } from './FragmentManager.js';
import { FragmentRenderer } from './renderer/FragmentRenderer.js';
import { bound } from '../decorators/bound.js';
import { EventTypes } from '../events/EventTypes.js';
/**
 * Similar to Activity, with lifecycle methods (onCreate, onAttach, onDetach, etc.).
 * The crucial difference is that a Fragment is always hosted by an `Activity` or
 * another `Fragment`.
 *
 * A `Fragment` represents a reusable portion of the app's UI.
 * A fragment defines and manages its own layout, has its own lifecycle,
 * and can handle its own input events.
 * Fragments can't live on their own. They must be hosted by an activity or another
 * fragment. The fragment’s view hierarchy becomes part of, or attaches to,
 * the host’s view hierarchy.
 */
let Fragment = (() => {
    let _classSuper = EventTarget;
    let _instanceExtraInitializers = [];
    let _handleIntentEvent_decorators;
    return class Fragment extends _classSuper {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
            _handleIntentEvent_decorators = [bound];
            __esDecorate(this, null, _handleIntentEvent_decorators, { kind: "method", name: "handleIntentEvent", static: false, private: false, access: { has: obj => "handleIntentEvent" in obj, get: obj => obj.handleIntentEvent }, metadata: _metadata }, null, _instanceExtraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        state = (__runInitializers(this, _instanceExtraInitializers), FragmentState.Initialized);
        parent;
        children = new Map();
        fragmentManager;
        /**
         * The request code used to start an activity for a result.
         */
        requestCode = -1;
        /**
         * A list of pending activity results that were requested by the components
         * hosted in this fragment.
         * The key is the request code and the value contains the callback
         * that will be called when the activity result is received.
         * The callback is called with the result code and the resulting intent.
         */
        pendingActivityResult = new Map();
        #renderer;
        get renderer() {
            return this.#renderer;
        }
        constructor(options) {
            super();
            this.parent = options?.parent;
            this.fragmentManager = new FragmentManager(this);
            this.#renderer = new FragmentRenderer(this);
        }
        /**
         * When implemented, this method returns the class name to be used as a root class for the fragment.
         * This class name can be used to apply styles or identify the fragment in the DOM.
         * @returns The class name to be used as a root class for the fragment.
         */
        rootClass() {
            return undefined;
        }
        /**
         * @see {@link ./Renderer.js}
         */
        setRenderRoot(renderRoot) {
            this.#renderer.setRenderRoot(renderRoot);
        }
        onData(data) {
            this.dispatchEvent(new CustomEvent('fragment:data', { detail: data }));
        }
        /**
         * Called once when the fragment is created.
         *
         * In the `onCreate()` method, perform basic fragment startup logic that happens only once
         * for the entire life of the fragment.
         * @param data Optional init data.
         */
        onCreate(data) {
            this.dispatchEvent(new CustomEvent('fragment:create', { detail: data }));
        }
        /**
         * Called when the fragment is attached to a parent
         */
        onAttach(data) {
            this.dispatchEvent(new CustomEvent('fragment:attach', { detail: data }));
        }
        /**
         * Called when the fragment becomes visible.
         */
        onStart() {
            this.dispatchEvent(new CustomEvent('fragment:start', { detail: null }));
        }
        /**
         * Called when the fragment gains focus.
         */
        onResume() {
            this.dispatchEvent(new CustomEvent('fragment:resume', { detail: null }));
        }
        /**
         * Called when the fragment loses focus.
         */
        onPause() {
            this.dispatchEvent(new CustomEvent('fragment:pause', { detail: null }));
        }
        /**
         * Called when the fragment is no longer visible.
         */
        onStop() {
            this.dispatchEvent(new CustomEvent('fragment:stop', { detail: null }));
        }
        /**
         * Called when the fragment is detached from a parent
         */
        onDetach() {
            this.dispatchEvent(new CustomEvent('fragment:detach', { detail: null }));
        }
        /**
         * Called before the fragment is destroyed.
         */
        onDestroy() {
            this.dispatchEvent(new CustomEvent('fragment:destroy', { detail: null }));
            this.fragmentManager.onDestroy();
        }
        /**
         * Called by the renderer when the fragment is rendered for the first time.
         */
        onFirstRender() {
            //
        }
        /**
         * A function called when it should render the view.
         * By default it renders a fragment view if the fragment hosts other fragments and only one is visible.
         */
        render() {
            const fragment = this.getSingleVisibleFragment();
            if (fragment) {
                // we can manage the default rendering.
                // we make an assumption that by default only one fragment is rendered,
                // otherwise the parent activity or fragment would have to setup render roots.
                return fragment.render();
            }
            return nothing;
        }
        /**
         * Checks whether there's only one `Fragment` that is in the `Resumed` state
         * and returns it. It returns `null` if there is no `Fragment`s or more than a single
         * visible `Fragment`.
         * @returns A Fragment that is the only fragment that should be visible.
         */
        getSingleVisibleFragment() {
            const fragments = this.getVisibleFragments();
            return fragments.length === 1 ? fragments[0] : null;
        }
        /**
         * Checks whether this fragment is a host for other fragments.
         * @returns `true` when this fragment hosts other fragments.
         */
        isRenderingFragments() {
            return this.children.size > 0;
        }
        /**
         * @returns The list of all fragments that are in the Resumed state.
         */
        getVisibleFragments() {
            return this.fragmentManager.getVisible();
        }
        /**
         * Adds a child fragment to this fragment.
         * The FragmentManager will handle lifecycle.
         * @param key The name of the fragment.
         * @param fragment The fragment to add.
         */
        async addChild(key, fragment, data) {
            this.children.set(key, fragment);
            await this.fragmentManager.attachFragment(key, fragment, this, data);
        }
        /**
         * Removes a fragment from this fragment.
         * @param key The name of the fragment to remove.
         */
        async removeChild(key) {
            const child = this.children.get(key);
            if (child) {
                await this.fragmentManager.detachFragment(key);
                this.children.delete(key);
            }
        }
        /**
         * A helper to request an update.
         * Application or parent Fragment will handle rendering.
         */
        requestUpdate(opts = {}) {
            const { fragment = true } = opts;
            if (fragment) {
                this.#renderer.requestUpdate();
            }
            this.parent?.requestUpdate(opts);
            // If no parent, do nothing (or maybe throw an error?)
        }
        getApplication() {
            return this.parent?.getApplication();
        }
        getActivity() {
            return this.parent?.getActivity();
        }
        /**
         * Starts an activity for result.
         * @param intent The intent to start.
         */
        async startActivityForResult(intent) {
            const activity = this.getActivity();
            if (!activity) {
                throw new Error(`The fragment has no activity. Unable to start an intent.`);
            }
            this.requestCode = await activity.startActivityForResult(intent);
        }
        /**
         * Starts another activity.
         * @param intent The intent to start.
         */
        async startActivity(intent) {
            const activity = this.getActivity();
            if (!activity) {
                throw new Error(`The fragment has no activity. Unable to start an intent.`);
            }
            await activity.startActivity(intent);
        }
        /**
         * The callback method that is triggered when another activity that was
         * started for result finishes.
         *
         * @param requestCode The request code that was used to start the activity.
         * @param data The data that was passed back.
         * @param intent The intent that was used to start the activity.
         */
        async onActivityResult(requestCode, resultCode, intent) {
            const { pendingActivityResult, children } = this;
            if (pendingActivityResult.has(requestCode)) {
                const { onResult } = pendingActivityResult.get(requestCode);
                pendingActivityResult.delete(requestCode);
                onResult(resultCode, intent);
                return;
            }
            for (const fragment of children.values()) {
                if (fragment.hasRequestCode(requestCode)) {
                    fragment.onActivityResult(requestCode, resultCode, intent);
                    return;
                }
            }
            this.requestCode = -1;
            if (this.constructor === Fragment) {
                // eslint-disable-next-line no-console
                console.info('Fragment#onActivityResult() not implemented', requestCode, resultCode, intent);
            }
        }
        hasRequestCode(requestCode) {
            for (const fragment of this.children.values()) {
                if (fragment.hasRequestCode(requestCode)) {
                    return true;
                }
            }
            return this.requestCode === requestCode;
        }
        /**
         * A handler for the intent event dispatched by web components hosted by this fragment.
         *
         * **Usage example:**
         *
         * ```ts
         * <custom-element @startactivity="${this.handleIntentEvent}"></custom-element>
         * <custom-element @startactivityforresult="${this.handleIntentEvent}"></custom-element>
         * ```
         *
         * @param event The event that was dispatched.
         * @returns A promise that resolves when the intent is handled.
         * @throws An error if the fragment has no activity.
         * @throws An error if the event type is not recognized.
         */
        async handleIntentEvent(event) {
            const activity = this.getActivity();
            if (!activity) {
                throw new Error(`The fragment has no activity. Unable to start an intent.`);
            }
            if (event.type === EventTypes.Intent.startActivityForResult) {
                const info = event.detail;
                const code = await activity.startActivityForResult(info.intent);
                this.requestCode = code;
                this.pendingActivityResult.set(code, {
                    onResult: info.onResult,
                });
            }
            else if (event.type === EventTypes.Intent.startActivity) {
                await activity.startActivity(event.detail.intent);
            }
            else {
                throw new Error(`Unrecognized intent event: ${event.type}`);
            }
        }
    };
})();
export { Fragment };
//# sourceMappingURL=Fragment.js.map