import { nanoid } from '../nanoid.js';
import { ProjectParent } from './ProjectParent.js';
import { ProjectItem } from './ProjectItem.js';
import { Kind as ProjectRequestKind } from './ProjectRequest.js';
import { Thing } from './Thing.js';
import { ProjectFolderKind } from './kinds.js';
export const DefaultFolderName = 'New folder';
/**
 * Represents a folder, a group of requests or other folders, in a folder.
 */
export class ProjectFolder extends ProjectParent {
    /**
     * The default name of the folder.
     */
    static get defaultName() {
        return DefaultFolderName;
    }
    kind = ProjectFolderKind;
    /**
     * A reference to the top level project object.
     */
    project;
    /**
     * Timestamp when the folder was last updated.
     */
    updated = 0;
    /**
     * Timestamp when the folder was created.
     */
    created = 0;
    constructor(project, input) {
        super();
        this.project = project;
        let init;
        if (typeof input === 'string') {
            init = JSON.parse(input);
        }
        else if (typeof input === 'object') {
            init = input;
        }
        else {
            const now = Date.now();
            init = {
                kind: ProjectFolderKind,
                info: Thing.fromName(DefaultFolderName).toJSON(),
                created: now,
                updated: now,
                items: [],
                key: nanoid(),
            };
        }
        this.new(init);
    }
    /**
     * Creates a new project folder clearing anything that is so far defined.
     *
     * Note, this throws an error when the project folder is not a project folder.
     */
    new(init) {
        if (!ProjectFolder.isProjectFolder(init)) {
            throw new Error(`Not a project folder.`);
        }
        const { key = nanoid(), created = Date.now(), updated = Date.now(), items, info } = init;
        this.kind = ProjectFolderKind;
        this.key = key;
        this.created = created;
        this.updated = updated;
        if (Array.isArray(items)) {
            this.items = items.map((i) => new ProjectItem(this.project, i));
        }
        else {
            this.items = [];
        }
        if (info) {
            this.info = new Thing(info);
        }
        else {
            this.info = Thing.fromName(DefaultFolderName);
        }
    }
    /**
     * Checks whether the input is a definition of a project folder.
     */
    static isProjectFolder(input) {
        const typed = input;
        if (!input || typed.kind !== ProjectFolderKind) {
            return false;
        }
        return true;
    }
    toJSON() {
        const result = {
            kind: ProjectFolderKind,
            info: this.info.toJSON(),
            key: this.key,
            created: this.created,
            updated: this.updated,
            items: [],
        };
        if (Array.isArray(this.items)) {
            result.items = this.items.map((i) => i.toJSON());
        }
        return result;
    }
    /**
     * Creates a new ProjectFolder object from a name.
     * @param project The top-most project.
     * @param name The name to set.
     */
    static fromName(project, name = DefaultFolderName) {
        const now = Date.now();
        const key = nanoid();
        const info = Thing.fromName(name);
        const definition = new ProjectFolder(project, {
            key,
            created: now,
            updated: now,
            items: [],
            kind: ProjectFolderKind,
            info: info.toJSON(),
        });
        return definition;
    }
    /**
     * Appends a new folder to the folder. It updates the project to add the request definition.
     * @param name The name to set. Optional.
     * @returns The key of newly inserted folder.
     */
    addFolder(name) {
        return this.project.addFolder(name, { parent: this.key });
    }
    /**
     * Appends a new request to the folder. It updates the project to add the request definition.
     * @param request The request to append to the folder.
     * @returns The key of newly inserted request.
     */
    addRequest(request, opts = {}) {
        const addOptions = { parent: this.key, ...opts };
        if (typeof request === 'string') {
            return this.project.addRequest(request, addOptions);
        }
        return this.project.addRequest(request, addOptions);
    }
    /**
     * Adds an environment to the project.
     * @param env The definition of the environment to use to create the environment or it's name
     * @returns The same or created environment.
     */
    addEnvironment(env, opts = {}) {
        const newOptions = { ...opts, parent: this.key };
        return this.project.addEnvironment(env, newOptions);
    }
    /**
     * Lists items (not the actual definitions!) that are folders.
     */
    listFolderItems() {
        const { items = [] } = this;
        return items.filter((i) => i.kind === ProjectFolderKind);
    }
    /**
     * Lists items (not the actual definitions!) that are requests.
     */
    listRequestItems() {
        const { items = [] } = this;
        return items.filter((i) => i.kind === ProjectRequestKind);
    }
    /**
     * Lists folders in this folder.
     */
    listFolders() {
        return this.project.listFolders({ folder: this.key });
    }
    /**
     * Lists requests in this folder.
     */
    listRequests() {
        return this.project.listRequests(this.key);
    }
    /**
     * The callback called when the object is attached to a parent.
     * This is called when the object is created and inserted to a project or a folder
     * and when the object is moved between folders.
     */
    attachedCallback() {
        // ...
    }
    /**
     * The callback called when the object is detached from its parent.
     * This callback is called when the item is deleted from a folder or a project,
     * or when the item is about to be moved to another folder.
     */
    detachedCallback() {
        // ...
    }
    /**
     * @returns The instance of the HttpProject or a ProjectFolder that is a closes parent of this instance.
     */
    getParent() {
        const { project, key } = this;
        return project.findParent(key);
    }
    /**
     * @returns A reference to the parent folder or the top-level HTTP project.
     */
    getProject() {
        return this.project;
    }
    /**
     * Removes this folder from the project.
     */
    remove() {
        this.project.removeFolder(this.key);
    }
    /**
     * @returns The list of environments defined in this folder
     */
    getEnvironments() {
        return this.project.getEnvironments({ parent: this.key });
    }
    /**
     * @param key The environment key to read.
     */
    getEnvironment(key) {
        return this.project.getEnvironment(key, { parent: this.key });
    }
    /**
     * Removes an environment from the folder or a sub-folder.
     *
     * @param key the key of the environment to remove
     * @returns The removed environment, if any.
     */
    removeEnvironment(key) {
        return this.project.removeEnvironment(key, { parent: this.key });
    }
    /**
     * This is a link to the `getEnvironments()`. The difference is that on the
     * project level it won't return environments defined with the class initialization.
     */
    listEnvironments() {
        return this.project.listEnvironments({ parent: this.key });
    }
    /**
     * Makes a copy of this folder.
     * By default it attaches the copied folder to the same parent.
     * It also, by default, copies requests declared in this folder.
     *
     * Use the options dictionary to control these behaviors.
     *
     * @param opts Cloning options
     */
    clone(opts = {}) {
        const { targetProject = this.project, targetFolder } = opts;
        const copy = new ProjectFolder(targetProject, this.toJSON());
        copy.key = nanoid();
        const extProject = targetProject !== this.project;
        if (extProject) {
            if (targetFolder) {
                const parent = targetProject.findFolder(targetFolder, { keyOnly: true });
                if (!parent) {
                    throw new Error(`The target project does not contain the folder ${targetFolder}`);
                }
                parent.addFolder(copy);
            }
            else {
                targetProject.addFolder(copy);
            }
        }
        else {
            const parent = targetFolder ? this.project.findFolder(targetFolder, { keyOnly: true }) : this.getParent();
            if (parent) {
                parent.addFolder(copy);
            }
            else {
                throw new Error(`Unable to locate a parent of the folder.`);
            }
        }
        // removes all items. Depending on the passed option we re-add them next.
        copy.items = [];
        if (!opts.withoutRequests) {
            this.cloneRequests(copy, this.project);
        }
        if (!opts.withoutFolders) {
            this.cloneSubFolders(copy, this.project, !opts.withoutRequests);
        }
        return copy;
    }
    /**
     * Clones the current requests to the target folder.
     *
     * @param folder The target folder into which to put the requests. The folder has to have
     *               the target project attached to it.
     * @param project The originating project where the definitions are stored
     */
    cloneRequests(folder, project) {
        const requests = this.items.filter((i) => i.kind === ProjectRequestKind);
        requests.forEach((r) => {
            const request = project.findRequest(r.key, { keyOnly: true });
            if (!request) {
                // Should we throw an error here?
                // CONS:
                // - It's not really related to the operation. It means there is an inconsistency in the project.
                //   That's the role of the project class.
                // - Ignoring this would allow us to make a copy that is error free.
                // - The error may occur in a situation when the user does not expect it (giving the nature of the error)
                // Pros:
                // - There's an inconsistency in the project definition that should be reported back to the UI
                //   for the user to inspect
                return;
            }
            const copy = request.clone({ withoutAttach: true });
            copy.project = folder.getProject();
            folder.addRequest(copy);
        });
    }
    /**
     * Clones the sub-folders to the target folder.
     *
     * @param folder The target folder into which to put the sub-folders. The folder has to have the target project
     *               attached to it.
     * @param project The originating project where the definitions are stored
     * @param withRequests Whether to clone requests with the folder.
     */
    cloneSubFolders(folder, project, withRequests = true) {
        const folders = this.items.filter((i) => i.kind === ProjectFolderKind);
        folders.forEach((f) => {
            const definition = project.findFolder(f.key, { keyOnly: true });
            if (!definition) {
                return;
            }
            const copy = new ProjectFolder(folder.getProject(), definition.toJSON());
            copy.key = nanoid();
            copy.items = [];
            folder.addFolder(copy);
            if (withRequests) {
                definition.cloneRequests(copy, project);
            }
            definition.cloneSubFolders(copy, project, withRequests);
        });
    }
}
//# sourceMappingURL=ProjectFolder.js.map