import { MarkupEntity, MarkupEntityConstructorArgs } from './MarkupEntity';
import { MarkupEntityBase } from './MarkupEntityBase';
import { v4 as uuidv4 } from 'uuid';
import { MarkupSheet2DCollection } from './MarkupSheet2DCollection';
import { MergableObjectEventArgs, MergableObjectState, MergableObjectWithState } from '../../MergableSet';
import { MarkupEventTypes, MarkupProperty } from '../MarkupEntityTypes';

/**
 * Class representing a markup sheet in the markup API.
 */
export class MarkupSheet2D extends MarkupEntityBase {
    public isEnabled = true;
    /**
     * Do not use. Use {@link MarkupSheet2DCollection.addSheet} instead.
     * @hidden
     */
    constructor(
        id: string,
        readonly parent: MarkupSheet2DCollection,
        properties = new Map<string, MarkupProperty>()
    ) {
        super(id, properties);
    }

    onAdded(o: MergableObjectEventArgs<MarkupSheet2D>): void {
        this.parent.observable.notifyObservers({ eventType: MarkupEventTypes.Added, ...o });
    }

    onUpdate(o: MergableObjectEventArgs<MarkupSheet2D>): void {
        this.parent.observable.notifyObservers({ eventType: MarkupEventTypes.Update, ...o });
    }

    onDelete(o: MergableObjectEventArgs<MarkupSheet2D>): void {
        this.parent.observable.notifyObservers({ eventType: MarkupEventTypes.Delete, ...o });
    }

    isEqual(o: MarkupSheet2D): boolean {
        return (
            o.id === this.id &&
            o.properties.size === this.properties.size &&
            [...this.properties].every(([key, value]) => value === o.properties.get(key))
        );
    }

    clone(): MarkupSheet2D {
        return new MarkupSheet2D(this.id, this.parent, this.properties);
    }

    *[Symbol.iterator](): IterableIterator<MarkupEntity> {
        for (const entity of this.entites()) {
            yield entity;
        }
    }

    /**
     * @param states Array of states used to filter out entities, default is [MergableObjectState.Added, MergableObjectState.Modified, MergableObjectState.Unchanged].
     * @example
     * ```typescript
     * // Loops all modified entities in this sheet.
     * for(const modifiedEntity of sheet.entities([MergableObjectState.Modified])) {
     *  modifiedEntity.x = 10;
     *  modifiedEntity.update();
     * }
     * ```
     */
    *entites(states: MergableObjectState[] = []): IterableIterator<MarkupEntity> {
        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.parent._layer.objects) {
            if (object.local instanceof MarkupEntity && object.local.sheet === this && statesSet.has(object.state)) {
                yield object.local;
            }
        }
    }

    /**
     * @param ctor Entity type to add to the sheet
     * @param entitySpecifics specific parameters for that entity type.
     * @returns a new entity that has been added to the sheet.
     */
    add<ARG, RET extends MarkupEntity>(
        ctor: new (args: MarkupEntityConstructorArgs & ARG) => RET,
        entitySpecifics: ARG,
        properties = new Map<string, MarkupProperty>()
    ): RET {
        const id = uuidv4();
        const arg = {
            id,
            sheet: this,
            properties: properties,
            ...entitySpecifics
        };
        const entity = new ctor(arg);
        this.parent._layer.add(entity);
        return entity;
    }

    /**
     * Get a markup entity based on an id.
     * @param entityOrId id of markup entity you want to get.
     * @returns The markup entity corralating to the id if it exists in the sheet.
     */
    get(entityOrId: Pick<MarkupEntity, 'id'> | string): MergableObjectWithState<MarkupEntity> | undefined {
        entityOrId = typeof entityOrId === 'string' ? entityOrId : entityOrId.id;
        const e = this.parent._layer.get(entityOrId);
        if (e?.local instanceof MarkupEntity && e.local.sheet === this) {
            return e as unknown as MergableObjectWithState<MarkupEntity>;
        }
        return undefined;
    }

    /**
     * Marks the sheet as updated in the {@link MarkupSheet2DCollection}.
     */
    markAsUpdated(): boolean {
        return this.parent._layer.update(this.id, () => {});
    }

    /**
     * Mark this sheet to be deleted on the next {@link MarkupSheet2DCollection.saveAndMergeSheets}.
     * @returns true if sheet was deleted, false if it failed to delete.
     */
    markAsDeleted(): boolean {
        for (const entity of this) {
            entity.markAsDeleted();
        }

        return this.parent._layer.delete(this);
    }

    /**
     * Duplicates the sheet and all its entities.
     * @param id Id of the new duplicate sheet.
     * @returns The new duplicate sheet.
     */
    duplicate(id: string): MarkupSheet2D {
        if (this.parent._layer.get(id)) {
            throw new Error(`A sheet with id ${id} already exists.`);
        }

        const newSheet = this.parent.addSheet(id, new Map(this.properties));

        for (const entity of this) {
            entity.copyTo(newSheet);
        }

        return newSheet;
    }
}
