// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT

import {
    AttrInputType, LabelType, SerializedAttribute, SerializedLabel,
} from 'server-response-types';
import { ShapeType, AttributeType } from './enums';
import { ArgumentError } from './exceptions';

export class Attribute {
    public id?: number;
    public defaultValue: string;
    public inputType: AttrInputType;
    public mutable: boolean;
    public name: string;
    public values: string[];

    constructor(initialData: SerializedAttribute) {
        const data = {
            id: undefined,
            default_value: undefined,
            input_type: undefined,
            mutable: undefined,
            name: undefined,
            values: undefined,
        };

        for (const key in data) {
            if (Object.prototype.hasOwnProperty.call(data, key)) {
                if (Object.prototype.hasOwnProperty.call(initialData, key)) {
                    if (Array.isArray(initialData[key])) {
                        data[key] = [...initialData[key]];
                    } else {
                        data[key] = initialData[key];
                    }
                }
            }
        }

        if (!Object.values(AttributeType).includes(data.input_type)) {
            throw new ArgumentError(`Got invalid attribute type ${data.input_type}`);
        }

        Object.defineProperties(
            this,
            Object.freeze({
                id: {
                    get: () => data.id,
                },
                defaultValue: {
                    get: () => data.default_value,
                },
                inputType: {
                    get: () => data.input_type,
                },
                mutable: {
                    get: () => data.mutable,
                },
                name: {
                    get: () => data.name,
                },
                values: {
                    get: () => [...data.values],
                },
            }),
        );
    }

    toJSON(): SerializedAttribute {
        const object: SerializedAttribute = {
            name: this.name,
            mutable: this.mutable,
            input_type: this.inputType,
            default_value: this.defaultValue,
            values: this.values,
        };

        if (typeof this.id !== 'undefined') {
            object.id = this.id;
        }

        return object;
    }
}

export class Label {
    public name: string;
    public readonly id?: number;
    public readonly color?: string;
    public readonly attributes: Attribute[];
    public readonly type: LabelType;
    public structure: {
        sublabels: Label[];
        svg: string;
    } | null;
    public deleted: boolean;
    public patched: boolean;
    public readonly hasParent?: boolean;

    constructor(initialData: SerializedLabel) {
        const data = {
            id: undefined,
            name: undefined,
            color: undefined,
            type: undefined,
            structure: undefined,
            has_parent: false,
            deleted: false,
            patched: false,
            svg: undefined,
            elements: undefined,
            sublabels: undefined,
            attributes: [],
        };

        for (const key of Object.keys(data)) {
            if (Object.prototype.hasOwnProperty.call(initialData, key)) {
                data[key] = initialData[key];
            }
        }

        data.attributes = [];

        if (
            Object.prototype.hasOwnProperty.call(initialData, 'attributes') &&
            Array.isArray(initialData.attributes)
        ) {
            for (const attrData of initialData.attributes) {
                data.attributes.push(new Attribute(attrData));
            }
        }

        if (data.type === 'skeleton') {
            data.sublabels = data.sublabels.map((internalLabel) => new Label({ ...internalLabel, has_parent: true }));
        }

        Object.defineProperties(
            this,
            Object.freeze({
                id: {
                    get: () => data.id,
                },
                name: {
                    get: () => data.name,
                    set: (name) => {
                        if (typeof name !== 'string') {
                            throw new ArgumentError(`Name must be a string, but ${typeof name} was given`);
                        }
                        data.name = name;
                        if (Number.isInteger(data.id)) {
                            data.patched = true;
                        }
                    },
                },
                color: {
                    get: () => data.color,
                    set: (color) => {
                        if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) {
                            data.color = color;
                            if (Number.isInteger(data.id)) {
                                data.patched = true;
                            }
                        } else {
                            throw new ArgumentError('Trying to set wrong color format');
                        }
                    },
                },
                attributes: {
                    get: () => [...data.attributes],
                },
                type: {
                    get: () => data.type,
                },
                structure: {
                    get: () => {
                        if (data.type === ShapeType.SKELETON) {
                            return {
                                svg: data.svg,
                                sublabels: [...data.sublabels],
                            };
                        }

                        return null;
                    },
                },
                deleted: {
                    get: () => data.deleted,
                    set: (value) => {
                        data.deleted = value;
                    },
                },
                patched: {
                    get: () => data.patched,
                    set: (value) => {
                        data.patched = value;
                    },
                },
                hasParent: {
                    get: () => data.has_parent,
                },
            }),
        );
    }

    toJSON(): SerializedLabel {
        const object: SerializedLabel = {
            name: this.name,
            attributes: [...this.attributes.map((el) => el.toJSON())],
            type: this.type,
        };

        if (typeof this.color !== 'undefined') {
            object.color = this.color;
        }

        if (typeof this.id !== 'undefined') {
            object.id = this.id;
        }

        if (this.type) {
            object.type = this.type;
        }

        const { structure } = this;
        if (structure) {
            object.svg = structure.svg;
            object.sublabels = structure.sublabels.map((internalLabel) => internalLabel.toJSON());
        }

        return object;
    }
}
