import { MarkupSheet2D } from './MarkupSheet2D';

import { LayerInterface, LayerFailure } from '../Layer';
import { BimChangeBlob, BimChangeDwg } from '../../loader/bim-api-client';
import { MarkupEntityBase } from './MarkupEntityBase';
import { MergableObjectWithState, MergableObjectState } from '../../MergableSet';
import { Observable } from '../../loader/babylonjs-import';
import { MarkupLayerEvent2D, MarkupProperty } from '../MarkupEntityTypes';
import { Permissions } from '../../loader/Permission';

/**
 * Collection of all the {@link MarkupSheet2D} instances that have been created locally and loaded via the markup API.
 */
export class MarkupSheet2DCollection {
    /**
     * Do not use this directly, use {@link MarkupSheet2DCollection.saveAndMerge} etc instead.
     * @hidden
     */
    public _layer!: LayerInterface<MarkupEntityBase>;

    /**
     * Do not use. This is created automatically when you call {@link MarkupSheets2DApi.getOrLoadSheets}.
     * @hidden
     */
    public constructor(
        public readonly observable: Observable<MarkupLayerEvent2D>,
        public readonly attachedTo: BimChangeDwg | BimChangeBlob
    ) {}

    *[Symbol.iterator](): IterableIterator<MarkupSheet2D> {
        for (const sheet of this.sheets()) {
            yield sheet;
        }
    }

    /**
     * @param states Array of states used to filter out sheets,
     * default is [MergableObjectState.Added, MergableObjectState.Modified, MergableObjectState.Unchanged].
     *
     * @example
     * ```typescript
     * // Loops all modified sheets in this collection.
     * for(const modifiedSheets of collection.sheets([MergableObjectState.Modified])) {
     *  ...
     * }
     * ```
     */
    *sheets(states: MergableObjectState[] = []): IterableIterator<MarkupSheet2D> {
        const statesSet = new Set(states);
        if (statesSet.size === 0) {
            statesSet.add(MergableObjectState.Added);
            statesSet.add(MergableObjectState.Modified);
            statesSet.add(MergableObjectState.Unchanged);
        }
        for (const object of this._layer.objects) {
            if (object.local instanceof MarkupSheet2D && statesSet.has(object.state)) {
                yield object.local;
            }
        }
    }

    /**
     * Creates a new markup sheet and adds it to the layer api.
     * @param id The id for the new sheet (has to be unique).
     * @param properties Properties for the sheet.
     * @returns The new markup sheet.
     */
    addSheet(id: string, properties?: Map<string, MarkupProperty>): MarkupSheet2D {
        const existingSheet = this.get(id);
        if (existingSheet) {
            throw new Error(`Markup sheet with id "${id}" already exists.`);
        }

        const sheet = new MarkupSheet2D(id, this, properties);
        this._layer.add(sheet);
        return sheet;
    }

    /**
     * Get a markup sheet based on id.
     * @param entityOrId Id of the markup sheet you are trying to get.
     * @returns The markup sheet if it exists, undefined if no markup sheet of that id exists.
     */
    get(entityOrId: string | Pick<MarkupEntityBase, 'id'>): MergableObjectWithState<MarkupEntityBase> | undefined {
        return this._layer.get(typeof entityOrId === 'string' ? entityOrId : entityOrId.id);
    }

    /**
     * Clear the layer.
     */
    clear(): void {
        this._layer.clear();
    }

    /**
     * `true` if a layer exists and if {@link Layer.saveAndMerge} or {@link Layer.loadAndMerge}
     * has been called successfully at least once.
     */
    public get isLoaded(): boolean {
        return this._layer.isLoaded;
    }

    /**
     * Loads all existing (remote) objects from the backend and attempts to merge them into {@link objects}.
     * This may result in conflicts between the local and the remote objects. A typical example is if the same
     * object has been modified locally but also remotely (in the backend.)
     */
    public loadAndMerge(): Promise<number | LayerFailure> {
        return this._layer.loadAndMerge();
    }

    /**
     * Get the version of the currently loaded layer. [-1] if no layer has yet been loaded.
     */
    public get version(): number {
        return this._layer.version;
    }

    /**
     * Number of objects with conflicts in the layer. A conflict occurs if there is a mismatch between the local
     * and the remote representation of the layer. Conflicts can only occur when {@link Layer.saveAndMerge} or {@link Layer.loadAndMerge}
     * is called.
     */
    public get conflictCount(): number {
        return this._layer.conflictCount;
    }

    /**
     * Number of objects in layer not marked with state {@link MergableObjectState.Unchanged}.
     */
    public get changeCount(): number {
        return this._layer.changeCount;
    }

    /**
     * `true` if user has permission to update the layer.
     */
    public get permission(): Permissions {
        return this._layer.permission;
    }

    /**
     * Get the objects currently in the layer
     */
    public get objects(): IterableIterator<Readonly<MergableObjectWithState<MarkupEntityBase>>> {
        return this._layer.objects;
    }

    /**
     * Save the sheets using the layer api.
     * @param overwrite If true then the last version will be overwritten.
     * @returns Number of conflicts, if > 0 then save was not successfull.
     */
    public async saveAndMergeSheets(overwrite = false): Promise<number | LayerFailure> {
        if (this._layer.changeCount > 0) {
            return this._layer.saveAndMerge(overwrite);
        } else return 0;
    }

    /**
     * Conflicts in the layer. A conflict occurs if there is a mismatch between the local
     * and the remote representation of a object in the layer. Conflicts can only occur when {@link Layer.saveAndMerge} or {@link Layer.loadAndMerge}
     * is called.
     */

    public conflicts(): Readonly<MergableObjectWithState<MarkupEntityBase>>[] {
        return this._layer.conflicts;
    }
}
