import { asWriteable } from '../Types';
import { Permission, Permissions } from './Permission';
import { AvailableLayerDefinitions } from './bim-api-client';

/**
 * A Layer definition. Describes a layer and its permissions. To create a layer
 * the corresponding layer definition must exist.
 */
export class LayerDefinition extends Permissions {
    /** Used to represent a layer definition that does not exist. */
    public static readonly notFound = Object.freeze(new LayerDefinition('not-found'));

    public constructor(
        public readonly id: string,
        ...permissions: Permission[]
    ) {
        super(Permissions.join(permissions));
    }

    /** `true` if the layer definition exists, `false` otherwise. */
    public get exists(): boolean {
        return this !== LayerDefinition.notFound;
    }
}

Object.freeze(LayerDefinition); // Ensures nobody can modify LayerDefinition.notFound;

/**
 * Represents how layer definitions are defined in JSON.
 */
export interface LayerDefinitionsDTO {
    [layerDefinitionId: string]: {
        /** permission mask in the form of a bigint string */
        permissionsString: string;
    };
}

/** Represents a collection of {@link LayerDefinition} instances. */
export class LayerDefinitions {
    private readonly _definitions = new Map<string, LayerDefinition>();

    /**
     * Constructor.
     * @param definitions Definitions the collection shall consist of.
     */
    constructor(definitions: LayerDefinition[] = []) {
        for (const d of definitions) {
            this._definitions.set(d.id, d);
        }
    }

    /**
     * Creates a {@link LayerDefinitions} instance for a `Record<string, { permission_str: string }>` lookup where
     * the properties are the layer ids. This is how a {@link LayerDefinitions} instance is represented in JSON.
     * @param src Source to copy from.
     * @returns change with converted property.
     */
    public static fromDTO(src?: LayerDefinitionsDTO): LayerDefinitions {
        return new LayerDefinitions(
            Object.entries(src ?? {}).map(([id, def]) => new LayerDefinition(id, BigInt(def.permissionsString)))
        );
    }

    /**
     * Copies the layer definitions defined by `layerDefinitions` to the `dst`.availableLayerDefinitions property.
     * @param layerDefinitions Layer definitions to copy to `dst`.availableLayerDefinitions.
     * @param dst Object to assign property on.
     * @returns Reference to `dst`.
     */
    public static copyToRef<T extends AvailableLayerDefinitions>(
        layerDefinitions: { json?: LayerDefinitionsDTO } | LayerDefinitions,
        dst: T
    ): T {
        const defs =
            layerDefinitions instanceof LayerDefinitions
                ? new LayerDefinitions([...layerDefinitions])
                : LayerDefinitions.fromDTO(layerDefinitions.json);
        asWriteable(dst).availableLayerDefinitions = defs;
        return dst;
    }

    /**
     * Gets the {@link LayerDefinition} instance with the same id. If no such instance is found then `{@link LayerDefinition.notFound} is returned.
     * @param id Id of layer definition to get.
     * @returns {@link Layer} instance.
     */
    get(id: string): LayerDefinition {
        return this._definitions.get(id) ?? LayerDefinition.notFound;
    }

    /**
     * Iterates all contained {@link LayerDefinition}. Use this to
     * get hold of all {@link LayerDefinition} instances the collection consists of.
     */
    *[Symbol.iterator](): IterableIterator<LayerDefinition> {
        for (const def of this._definitions.values()) {
            yield def;
        }
    }
}
