import { SelectedScript } from './SelectedScript';
import { IUserSelection } from './IUserSelection';
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import { IScript } from '@/domain/IScript';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { ICategory } from '@/domain/ICategory';
import { ICategoryCollection } from '@/domain/ICategoryCollection';

export class UserSelection implements IUserSelection {
    public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
    private readonly scripts: IRepository<string, SelectedScript>;

    constructor(
        private readonly collection: ICategoryCollection,
        selectedScripts: ReadonlyArray<SelectedScript>) {
        this.scripts =  new InMemoryRepository<string, SelectedScript>();
        if (selectedScripts && selectedScripts.length > 0) {
            for (const script of selectedScripts) {
                this.scripts.addItem(script);
            }
        }
    }

    public areAllSelected(category: ICategory): boolean {
        if (this.selectedScripts.length === 0) {
            return false;
        }
        const scripts = category.getAllScriptsRecursively();
        if (this.selectedScripts.length < scripts.length) {
            return false;
        }
        return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
    }

    public isAnySelected(category: ICategory): boolean {
        if (this.selectedScripts.length === 0) {
            return false;
        }
        return this.selectedScripts.some((s) => category.includes(s.script));
    }

    public removeAllInCategory(categoryId: number): void {
        const category = this.collection.findCategory(categoryId);
        const scriptsToRemove = category.getAllScriptsRecursively()
            .filter((script) => this.scripts.exists(script.id));
        if (!scriptsToRemove.length) {
            return;
        }
        for (const script of scriptsToRemove) {
            this.scripts.removeItem(script.id);
        }
        this.changed.notify(this.scripts.getItems());
    }

    public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
        const category = this.collection.findCategory(categoryId);
        const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
            .filter((script) =>
                !this.scripts.exists(script.id)
                    || this.scripts.getById(script.id).revert !== revert,
            );
        if (!scriptsToAddOrUpdate.length) {
            return;
        }
        for (const script of scriptsToAddOrUpdate) {
            const selectedScript = new SelectedScript(script, revert);
            this.scripts.addOrUpdateItem(selectedScript);
        }
        this.changed.notify(this.scripts.getItems());
    }

    public addSelectedScript(scriptId: string, revert: boolean): void {
        const script = this.collection.findScript(scriptId);
        if (!script) {
            throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
        }
        const selectedScript = new SelectedScript(script, revert);
        this.scripts.addItem(selectedScript);
        this.changed.notify(this.scripts.getItems());
    }

    public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
        const script = this.collection.findScript(scriptId);
        const selectedScript = new SelectedScript(script, revert);
        this.scripts.addOrUpdateItem(selectedScript);
        this.changed.notify(this.scripts.getItems());
    }

    public removeSelectedScript(scriptId: string): void {
        this.scripts.removeItem(scriptId);
        this.changed.notify(this.scripts.getItems());
    }

    public isSelected(scriptId: string): boolean {
        return this.scripts.exists(scriptId);
    }

    /** Get users scripts based on his/her selections */
    public get selectedScripts(): ReadonlyArray<SelectedScript> {
        return this.scripts.getItems();
    }

    public selectAll(): void {
        for (const script of this.collection.getAllScripts()) {
            if (!this.scripts.exists(script.id)) {
                const selection = new SelectedScript(script, false);
                this.scripts.addItem(selection);
            }
        }
        this.changed.notify(this.scripts.getItems());
    }

    public deselectAll(): void {
        const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
        for (const scriptId of selectedScriptIds) {
            this.scripts.removeItem(scriptId);
        }
        this.changed.notify([]);
    }

    public selectOnly(scripts: readonly IScript[]): void {
        if (!scripts || scripts.length === 0) {
            throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
        }
        // Unselect from selected scripts
        if (this.scripts.length !== 0) {
            this.scripts.getItems()
                .filter((existing) => !scripts.some((script) => existing.id === script.id))
                .map((script) => script.id)
                .forEach((scriptId) => this.scripts.removeItem(scriptId));
        }
        // Select from unselected scripts
        const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
        for (const toSelect of unselectedScripts) {
            const selection = new SelectedScript(toSelect, false);
            this.scripts.addItem(selection);
        }
        this.changed.notify(this.scripts.getItems());
    }
}
