// @ts-nocheck
import {
    CalendarElement,
    CircularTextInputElement,
    CustomizationOptions,
    DatePickerOption,
    DropdownOption,
    DropdownValue,
    DynamicImageElement,
    ImagePlaceHolderElement,
    ImageUploadOption,
    SwatchOption,
    SwatchValue,
    Template,
    TextInputElement,
    TextInputOption,
    VectorElement
} from "./types";
import axios from 'axios';
import {
    getRenderConfigImage,
    getRenderConfigTextBox,
    getRenderConfigTextBoxCircular,
    getRenderConfigVectorEps,
    isNumeric,
} from "./helper";
import {ImageOptions} from "../images/types";
import Images from "../images";

type IOption = SwatchOption | DropdownOption | ImageUploadOption | TextInputOption | DatePickerOption;
type IElement = DynamicImageElement | TextInputElement | CircularTextInputElement | ImagePlaceHolderElement | VectorElement | CalendarElement;

class Customization {
    private template: Template;
    private options: IOption[];
    private readonly breadcrumbs: {
        id: number;
        name: string;
        slug: string,
        _lft: number,
        _rgt: number,
        url: string
    }[];
    private readonly isAllowReplaceBackground: boolean;
    private readonly baseUrl: string;
    private LibCaching = {} as { [key: string]: string };
    private groupId: string | number | undefined = undefined;
    private groupIds: (string | number)[] = [];
    private showElementIds: (string | number)[] = [];
    private hideElementIds: (string | number)[] = [];
    private selectedGroups = [] as { id: string; uuid: string, position: number }[];
    private colorGuideImageUrl: string = '';
    private spotifyToken: {access_token: string, expires_in: number} = { access_token: '', expires_in: 0 };
    private searchSongResult = [];
    private inputSearchSong = '';
    private readonly isShowExcessiveOption = false;

    // @ts-ignore
    private groups: any;
    constructor(customizationOptions: CustomizationOptions) {
        this.template = customizationOptions.template;
        this.options = customizationOptions.options;
        this.groups = JSON.parse(this.template?.groups || '{}');
        this.baseUrl = customizationOptions.baseUrl;
        this.breadcrumbs = customizationOptions.breadcrumbs;
        this.isAllowReplaceBackground = customizationOptions.isAllowReplaceBackground;
        this.isShowExcessiveOption = !!customizationOptions.isShowExcessiveOption;
        this.buildOptionSet();
        this.options.forEach((option) => {
            if (option.showSpotifyPlayer) {
                option.showSpotifyPlayer = false;
            }

            if (option.isShow) {
                if (option.type == 'swatch' || option.type == 'dropdown') {
                    this.changeSwatchOrDropdownValue(option as SwatchOption | DropdownOption);
                } else if (option.type == 'text-input') {
                    this.changeTextInputValue(option as TextInputOption);
                } else if (option.type == 'image-upload') {
                    this.changeImageUploadValue(option as ImageUploadOption);
                } else if (option.type == 'date-picker') {
                    this.changeDatePickerValue(option as DatePickerOption);
                }
            }
        });

    }

    getData() {
        return {
            template: this.template,
            options: this.options,
        }
    }

    async fetchImageUrlsFirstTime(): Promise<void> {
        if (!this.template) {
            return;
        }

        // fetch customily library
        await this.fetchCustomilyLibrary()
        for (const element of this.template.elements) {
            if ("images" in element.config && element.config.images.length > 0 && element.config.imageId && !element.config.libraryId) {
                const image = element.config.images.find(item => item.order == element.config.imageId);
                if (image) {
                    element.config.imageUrl = image.value;
                }
            }

            if ("fonts" in element.config && element.config.fonts.length > 0 && element.config.fontId) {
                const font = element.config.fonts.find(item => item.order == element.config.fontId);
                if (font) {
                    element.config.textConfig.font = font.value;
                }
            }

            if ("colors" in element.config && element.config.colors.length > 0 && element.config.colorId) {
                const color = element.config.colors.find(item => item.order == element.config.colorId);
                if (color) {
                    element.config.textConfig.fill = color.value;
                }
            }

            if ("vectors" in element.config && element.config.vectors.length > 0 && element.config.vectorId && !element.config.libraryId) {
                const vector = element.config.vectors.find(item => item.order == element.config.vectorId);
                if (vector) {
                    element.config.imageUrl = vector.value;
                }
            }
        }

        this.updateVisibleElement();
    }

    getTemplate(): Template {
        return this.template;
    }

    getOptions(): IOption[] {
        return this.options;
    }

    getElements(): IElement[] {
        return this.template?.elements;
    }

    setTemplate(template: Template): void {
        this.template = template;
    }

    setOptions(options: IOption[]): void {
        this.options = options;
    }

    async selectOptionValue(option: IOption, value: SwatchValue | DropdownValue | null): Promise<void> {
        this.buildOptionSet();
        if (!this.template) {
            return;
        }

        if (this.isChangeTemplateOption(option as SwatchOption | DropdownOption)) {
            // fetch and set new template
            // @ts-ignore
            const templateId = value.templateId;
            if (templateId) {
                const newTemplate = await axios.get(`${this.baseUrl}/template/${templateId}`);
                const templateData = newTemplate.data;
                if (templateData) {
                    this.template = templateData.result;
                    this.groups = JSON.parse(this.template?.groups || '{}');
                } else {
                    throw new Error('Template not found');
                }
            }
        }

        const childOptionIds = option.childOptionIds;
        if (childOptionIds && childOptionIds.length) {
            if (option.type == 'swatch' || option.type == 'dropdown') {
                for (let i = 0; i < childOptionIds.length; i++) {
                    if (childOptionIds[i] == option.id) continue;
                    const childOptionIndex = this.options.findIndex(function (o) {
                        return o.id == childOptionIds[i];
                    });
                    if (childOptionIndex == -1) continue;
                    const childValues = option.childOptionIdValues[childOptionIds[i]];
                    const optionValues = option.childOptionIdValues[option.id];
                    const valueIndex = optionValues.findIndex(function (v: any) {
                        return v == value.value;
                    });
                    let childValue;
                    if (this.options[childOptionIndex].type == 'swatch') {
                        childValue = this.options[childOptionIndex].swatchValues.find(function (v: any) {
                            return v.value == childValues[valueIndex];
                        });
                    } else if (this.options[childOptionIndex].type == 'dropdown') {
                        childValue = this.options[childOptionIndex].dropdownValues.find(function (v: any) {
                            return v.value == childValues[valueIndex];
                        });
                    }

                    if (childValue) {
                        this.options[childOptionIndex].currentValue = childValue.id;
                    }
                }
            } else if (option.type == 'text-input') {
                for (let i = 0; i < childOptionIds.length; i++) {
                    const childOptionIndex = this.options.findIndex(o => o.id == option.childOptionIds[i] && o.id != option.id);
                    if (childOptionIndex > -1) {
                        const maxLength = parseInt(this.options[childOptionIndex].config.max_length);
                        this.options[childOptionIndex].currentValue = option.currentValue.substr(0, maxLength);
                    }
                }
            }

            this.buildOptionSet();
        }

        this.options.forEach((option) => {
            if (option.isShow) {
                if (option.type == 'swatch' || option.type == 'dropdown') {
                    this.changeSwatchOrDropdownValue(option as SwatchOption | DropdownOption);
                } else if (option.type == 'text-input') {
                    this.changeTextInputValue(option as TextInputOption);
                } else if (option.type == 'image-upload') {
                    this.changeImageUploadValue(option as ImageUploadOption);
                } else if (option.type == 'date-picker') {
                    this.changeDatePickerValue(option as DatePickerOption);
                }
            }
        });

        // fetch customily library
        await this.fetchCustomilyLibrary();
        for (const element of this.template.elements) {
            if ("images" in element.config && element.config.images.length > 0 && element.config.imageId && !element.config.libraryId) {
                const image = element.config.images.find(item => item.order == element.config.imageId);
                if (image) {
                    element.config.imageUrl = image.value;
                }
            }

            if ("fonts" in element.config && element.config.fonts.length > 0 && element.config.fontId && !element.config.fontLibraryId) {
                const font = element.config.fonts.find(item => item.order == element.config.fontId);
                if (font) {
                    element.config.textConfig.font = font.value;
                }
            }

            if ("colors" in element.config && element.config.colors.length > 0 && element.config.colorId && !element.config.colorLibraryId) {
                const color = element.config.colors.find(item => item.order == element.config.colorId);
                if (color) {
                    element.config.textConfig.fill = color.value;
                }
            }

            if ("vectors" in element.config && element.config.vectors.length > 0 && element.config.vectorId && !element.config.libraryId) {
                const vector = element.config.vectors.find(item => item.order == element.config.vectorId);
                if (vector) {
                    element.config.imageUrl = vector.value;
                }
            }
        }

        this.updateVisibleElement();
    }

    private async fetchCustomilyLibrary() {
        if (!this.template) {
            return;
        }

        let elements = this.template.elements;
        let promises = [];
        for (const element of elements) {
            if ("images" in element.config && !("library" in element.config) && element.config.imageId && element.config.libraryId) {
                const libraryId = element.config.libraryId;
                const position = element.config.imageId;
                const libCacheKey = `${libraryId}-${position}`;
                const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
                const promise = new Promise<void>((resolve) => {
                    if (this.LibCaching[libCacheKey]) {
                        element.config.imageUrl = this.LibCaching[libCacheKey];

                        return resolve();
                    }

                    axios.get(endpoint).then((response) => {
                        const baseURL = "https://cdn.customily.com";
                        let pathProperty = 'Path';

                        if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
                            let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
                            this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
                            element.config.imageUrl = this.LibCaching[libCacheKey];

                            return resolve();
                        } else {
                            element.config.imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
                        }

                        return resolve();
                    })
                });
                promises.push(promise);
            }

            if ("colors" in element.config && !("color_library" in element.config) && element.config.colorId && element.config.colorLibraryId) {
                const libraryId = element.config.colorLibraryId;
                const position = element.config.colorId;
                const libCacheKey = `${libraryId}-${position}`;
                const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
                const promise = new Promise<void>((resolve) => {
                    if (this.LibCaching[libCacheKey]) {
                        if ("textConfig" in element.config) {
                            element.config.textConfig.fill = this.LibCaching[libCacheKey];
                        }

                        return resolve();
                    }

                    axios.get(endpoint).then((response) => {
                        let pathProperty = 'hex';
                        if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
                            this.LibCaching[libCacheKey] = response.data[pathProperty];
                            if ("textConfig" in element.config) {
                                element.config.textConfig.fill = this.LibCaching[libCacheKey];
                            }

                            return resolve();
                        }

                        return resolve();
                    });
                });
                promises.push(promise);
            }

            if ("fonts" in element.config && !("font_library" in element.config) && element.config.fontId && element.config.fontLibraryId) {
                const libraryId = element.config.fontLibraryId;
                const position = element.config.fontId;
                const libCacheKey = `${libraryId}-${position}`;
                const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
                const promise = new Promise<void>((resolve) => {
                    if (this.LibCaching[libCacheKey]) {
                        if ("textConfig" in element.config) {
                            element.config.textConfig.font = this.LibCaching[libCacheKey];
                        }

                        return resolve();
                    }

                    axios.get(endpoint).then((response) => {
                        const baseURL = "https://cdn.customily.com";
                        let pathProperty = 'Path';

                        if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
                            let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
                            this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
                            if ("textConfig" in element.config) {
                                element.config.textConfig.font = this.LibCaching[libCacheKey];
                                element.config.textConfig.fontFamily = 'customFont-' + response.data.fontId;
                                element.config.textConfig.fontFamilyDownload = this.LibCaching[libCacheKey];
                            }

                            return resolve();
                        }

                        return resolve();
                    });
                });
                promises.push(promise);
            }

            if ("vectors" in element.config && !("vector_library" in element.config) && element.config.vectorId && element.config.libraryId) {
                const libraryId = element.config.libraryId;
                const position = element.config.vectorId;
                const libCacheKey = `${libraryId}-${position}`;
                const endpoint = `https://app.customily.com/api/Libraries/${libraryId}/Elements/Position/${position}`;
                const promise = new Promise<void>((resolve) => {
                    if (this.LibCaching[libCacheKey]) {
                        element.config.imageUrl = this.LibCaching[libCacheKey];

                        return resolve();
                    }

                    axios.get(endpoint).then((response) => {
                        const baseURL = "https://cdn.customily.com";
                        let pathProperty = 'svgPath';

                        if (response.data && typeof response.data[pathProperty] != 'undefined' && response.data[pathProperty]) {
                            let contentReplace = response.data[pathProperty].startsWith('/') ? '/Content' : 'Content';
                            this.LibCaching[libCacheKey] = `${baseURL}${response.data[pathProperty].replace(contentReplace, '')}`;
                            element.config.imageUrl = this.LibCaching[libCacheKey];

                            return resolve();
                        } else {
                            element.config.imageUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
                        }

                        return resolve();
                    });
                });
                promises.push(promise);
            }
        }

        await Promise.all(promises);
    }

    private buildOptionSet() {
        // set currentValue for selected value
        this.options.forEach((option: IOption) => {
            if (option.type == 'swatch' || option.type == 'dropdown') {
                let selectedValue;
                if ("dropdownValues" in option) {
                    selectedValue = option.dropdownValues?.find((value: DropdownValue) => value.selected);
                } else if ("swatchValues" in option) {
                    selectedValue = option.swatchValues?.find((value: SwatchValue) => value.selected);
                }

                // @ts-ignore
                if (selectedValue && !Object.hasOwnProperty.call(option, 'currentValue')) {
                    // @ts-ignore
                    option.currentValue = isNaN(selectedValue.id) ? selectedValue.valueName : selectedValue.id;
                }

                if (selectedValue && Object.hasOwnProperty.call(selectedValue, 'groupId')) {
                    this.groupId = selectedValue.groupId;
                }
            }
        });
        // build isShow for option
        this.options.forEach((option: IOption) => {
            option.isShow = this.isShowOption(this.options, option);
            if (option.isShow) {
                if (this.isExcessiveSizeOption(option) || this.isExcessiveColorOption(option)) {
                    option.hideVisually = true;
                }
            }

            // hide option has label --------------
            if (/^-{5,}$/.test(option.label)) {
                option.hideVisually = true;
            }
        });

        // re-check isShow option
        this.options.forEach((optionItem: IOption) => {
            if (optionItem.isShow) {
                optionItem.isShow = this.isShowOption(this.options, optionItem);
            }
        });
    }

    private findOption(watchOptionId: number) {
        let watchOption = this.options.find(item => item.id == watchOptionId);
        if (!watchOption) {
            watchOption = this.options.find(item => item.id == this.groupId);
        }

        return watchOption;
    }

    private getConditionWatchOptionId(condition: any) {
        if (typeof condition.watchOptionId != 'undefined') {
            return condition.watchOptionId;
        }

        return condition.watch_option_id;
    }

    private getConditionDesiredValue(condition: any) {
        if (typeof condition.desiredValue != 'undefined') {
            return condition.desiredValue;
        }

        return condition.desired_value;
    }

    private getConditionCombinationOperator(condition: any) {
        return condition.combinationOperator || condition.combination_operator || 'and';
    }

    private getConditionAction(condition: any) {
        return (condition.action || 'show').toLowerCase();
    }

    private getOptionConditions(option: any) {
        const variationConditions = option.variationConditions;
        if (variationConditions && variationConditions.length) {
            return variationConditions;
        }

        return option.conditions || [];
    }

    /**
     * Resolve the value object an option is currently set to. Handles
     * both encodings of `currentValue` — numeric `id` (the common case
     * set by `buildOptionSet` and most host callers) and the
     * `valueName` fallback used when the value's id is non-numeric.
     *
     * Used by the condition evaluators below so a desiredValue
     * encoded as either the value's `id`, `valueName`, or `value`
     * matches reliably — Customily-authored conditions historically
     * mix all three encodings.
     */
    private resolveSelectedValue(option: IOption): SwatchValue | DropdownValue | null {
        const values: (SwatchValue | DropdownValue)[] =
            ("dropdownValues" in option && Array.isArray(option.dropdownValues) && option.dropdownValues) ||
            ("swatchValues" in option && Array.isArray(option.swatchValues) && option.swatchValues) ||
            [];
        if (!values.length) return null;
        // @ts-ignore — IOption union doesn't expose currentValue uniformly
        const cv = option.currentValue;
        return (
            values.find((v: any) => v && v.id === cv) ??
            values.find((v: any) => v && v.id == cv) ??
            values.find((v: any) => v && v.valueName === cv) ??
            values.find((v: any) => v && v.selected) ??
            null
        );
    }

    /**
     * Equality check for `desiredValue`. Customily's canonical convention
     * is that a condition's `desiredValue` matches the watched option's
     * selected value's `id`. When the option's values carry an `id`, the
     * matcher must restrict comparison to that field — falling through to
     * `value` / `valueName` on the same value object false-positives
     * whenever the `id` and `value` ranges overlap inside one option:
     *
     *   {id: 2, value: 0, valueName: "The Man"}       // ← user picked this
     *   {id: 0, value: 1, valueName: "Best Dad"}      // ← condition desired=[1]
     *
     * Without the restriction, Color sibling-options light up the wrong
     * branch ("Best Dad" picked → "Awesome Dad" image renders) because
     * `desired === selected.value` (1 === 1) hits even though the author
     * meant `desired === selected.id` (1 ≠ 0). Only when the value shape
     * doesn't ship an `id` (older Customily Color dropdowns expose just
     * `value` + `valueName`) do we fall back to those fields.
     */
    private matchesDesiredValue(watchOption: IOption, desiredValue: unknown): boolean {
        if (!watchOption) return false;
        // @ts-ignore
        const cv = watchOption.currentValue;
        const desired = Array.isArray(desiredValue) ? desiredValue : [desiredValue];
        // The "-1" wildcard means "any non-empty selection". Preserve
        // the original semantics — only fires when something is set.
        if (desired.includes(-1) && cv !== undefined && cv !== '') return true;

        const selected = this.resolveSelectedValue(watchOption);
        if (selected) {
            // @ts-ignore
            const hasId = selected.id !== undefined && selected.id !== null;
            if (hasId) {
                // id-only match — strict first, then loose for cross-type
                // tolerance (JSON sometimes ships `1` and `"1"` for the
                // same logical id).
                for (const d of desired) {
                    // @ts-ignore
                    if (d === selected.id) return true;
                    // @ts-ignore
                    if (d == selected.id) return true;
                }
                return false;
            }

            // No `id` → match by `value` then `valueName`. Same dual
            // strict/loose layout for type tolerance.
            for (const d of desired) {
                // @ts-ignore
                if (selected.value !== undefined && d === selected.value) return true;
                // @ts-ignore
                if (selected.value !== undefined && d == selected.value) return true;
                // @ts-ignore
                if (d === selected.valueName) return true;
            }
            return false;
        }

        // No resolvable value list (text-input / image-upload). Compare
        // directly against the option's raw `currentValue`.
        for (const d of desired) {
            // @ts-ignore
            if (d == cv) return true;
        }
        return false;
    }

    private getConditionValuesForOption(options: IOption[], conditions: any[]) {
        let conditionValues = [];
        for (const condition of conditions) {
            const watchOptionId = this.getConditionWatchOptionId(condition);
            if (typeof watchOptionId != 'undefined') {
                let watchOption = options.find(item => item.id == watchOptionId);
                const action = this.getConditionAction(condition);
                const desiredValue = this.getConditionDesiredValue(condition);
                if (watchOption && watchOption.isShow) {
                    if (this.matchesDesiredValue(watchOption, desiredValue)) {
                        conditionValues.push({
                            value: action === 'hide' ? false : true,
                            combinationOperator: this.getConditionCombinationOperator(condition),
                        });
                    } else {
                        conditionValues.push({
                            value: action === 'hide' ? true : false,
                            combinationOperator: this.getConditionCombinationOperator(condition),
                        })
                    }
                } else {
                    // if watchOptionId not found
                    // skip if conditions length > 1
                    if (conditions.length > 1 && !watchOption) {
                        continue;
                    }

                    let value = true;
                    if (action !== 'hide' && watchOption) {
                        value = false;
                    }

                    conditionValues.push({
                        value: value,
                        combinationOperator: this.getConditionCombinationOperator(condition),
                    });
                }
            }
        }

        return conditionValues;
    }

    private evaluateConditionList(options: IOption[], conditions: any[]) {
        if (conditions && conditions.length > 0) {
            return this.reduceConditionValues(this.getConditionValuesForOption(options, conditions));
        }

        return true;
    }

    private evaluateGroupConditions(option: IOption) {
        if (option.groupConditions && option.groupConditions.length) {
            for (const groupConditions of option.groupConditions) {
                let conditionsByGroup = [];
                let retValByGroup = false;
                for (let i = 0; i < groupConditions.length; i++) {
                    const condition = this.getConditionValue(groupConditions[i]);
                    if (condition) {
                        conditionsByGroup.push(condition);
                    }
                }

                // combination
                let finalCondition = true;
                if (conditionsByGroup.length) {
                    finalCondition = conditionsByGroup[0].value;
                    for (let i = 1; i < conditionsByGroup.length; i++) {
                        const condition = conditionsByGroup[i];
                        if (condition.operator === 'or') {
                            finalCondition = finalCondition || condition.value;
                        } else {
                            finalCondition = finalCondition && condition.value;
                        }
                    }
                }

                retValByGroup = finalCondition;
                if (retValByGroup) { // pass if any group condition is true
                    return true;
                }
            }

            return false;
        }

        return true;
    }

    private evaluateOptionConditions(options: IOption[], option: IOption) {
        const baseConditions = option.groupConditions && option.groupConditions.length
            ? this.evaluateGroupConditions(option)
            : this.evaluateConditionList(options, option.conditions || []);
        const variationConditions = this.evaluateConditionList(options, option.variationConditions || []);

        return baseConditions && variationConditions;
    }

    private reduceConditionValues(conditionValues: { value: boolean; combinationOperator: string }[]) {
        if (!conditionValues.length) {
            return true;
        }

        let finalCondition = conditionValues[0].value;
        for (let i = 1; i < conditionValues.length; i++) {
            const condition = conditionValues[i];
            if (condition.combinationOperator === 'or') {
                finalCondition = finalCondition || condition.value;
            } else {
                finalCondition = finalCondition && condition.value;
            }
        }

        return finalCondition;
    }

    private getConditionValue(condition: any) {
        const desiredValue = this.getConditionDesiredValue(condition);
        const watchOption = this.findOption(this.getConditionWatchOptionId(condition));

        if (typeof watchOption != 'undefined' && watchOption) {

            // Delegate to the shared matcher so groupConditions resolve
            // identically to regular conditions — historically this
            // branch read `watchOption.value` / `.valueName`, fields the
            // SDK never populates on options, so non-numeric desired
            // values silently never matched.
            let conditionValue = this.matchesDesiredValue(watchOption, desiredValue);

            if (this.getConditionAction(condition) === 'hide') {
                conditionValue = !conditionValue;
            }

            if (!watchOption.isShow) {
                conditionValue = this.getConditionAction(condition) === 'hide';
            }

            return {
                value: conditionValue,
                operator: this.getConditionCombinationOperator(condition)
            };

        }

        return null;
    }

    private isShowOption(options: IOption[], option: IOption): boolean {
        return this.evaluateOptionConditions(options, option);
    }

    private isExcessiveSizeOption(obj: IOption) {
        if (this.isShowExcessiveOption) {
            return false;
        }

        const isLabelSize = obj.label && obj.label.toLowerCase().includes('size');
        const sizeValues = ['S', 'M', 'L', 'XL', '2XL', '3XL', '4XL'];
        let valuesToCheck;
        if ('dropdownValues' in obj) {
            valuesToCheck = obj.dropdownValues;
        } else if ('swatchValues' in obj) {
            valuesToCheck = obj.swatchValues;
        }

        const areValuesSizes = valuesToCheck && valuesToCheck.some(value =>
            sizeValues.includes(value.valueName)
        );

        return isLabelSize && areValuesSizes;
    }

    private isExcessiveColorOption(obj: IOption) {
        if (this.isShowExcessiveOption) {
            return false;
        }

        const isLabelColor = obj.label && obj.label.toLowerCase().includes('color');
        const validateLabelColors = [
            'Shirt\'s Color', 'Hoodie\'s Color', 'Hoodie Color',
            'Sweatshirt\'s Color', 'Sweatshirt Color', 'Tank Top\'s Color', 'Tank Top Color',
            'T-Shirt\'s Color', 'T-Shirt Color', 'Polo\'s Color', 'Polo Color', 'Tee Color'
        ]

        let isTShirt = false;
        if (this.breadcrumbs && this.breadcrumbs.length > 2 && this.breadcrumbs[2].id == 7) {
            isTShirt = true;
        }

        let isValidLabelColor: boolean;

        if (isTShirt) {
            isValidLabelColor = validateLabelColors.some(label => obj.label.toLowerCase().includes(label.toLowerCase()));
        } else {
            isValidLabelColor = validateLabelColors.includes(obj.label);
        }

        return isLabelColor && isValidLabelColor;
    }

    private changeSwatchOrDropdownValue(option: SwatchOption | DropdownOption): void {
        if (!this.template) {
            return;
        }

        let values;
        if ("dropdownValues" in option) {
            values = option.dropdownValues;
        }

        if ("swatchValues" in option) {
            values = option.swatchValues;
        }

        if (!values) {
            return;
        }

        // Resolve which value the option is currently set to. Customily ships
        // values that may not carry an `id` (Color dropdowns we've seen only
        // expose `imageId`, `value`, `valueName`, `order`), so a strict
        // `item.id === currentValue` miss falls back to the still-`selected`
        // default — leaving `element.config.imageId` pinned to the original
        // image and silently hitting the library cache. Match against every
        // field the host might have stored in `currentValue`, with loose
        // equality so `"5"` and `5` interchange, and only fall back to the
        // selected flag when no field matches.
        const cv = option.currentValue;
        let value = values.find((item: SwatchValue | DropdownValue) => item.id !== undefined && item.id === cv);
        // eslint-disable-next-line eqeqeq
        if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.id !== undefined && item.id == cv);
        if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.valueName !== undefined && item.valueName === cv);
        // eslint-disable-next-line eqeqeq
        if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.value !== undefined && item.value == cv);
        if (!value) value = values.find((item: SwatchValue | DropdownValue) => item.selected);

        if (!value) {
            return;
        }

        // Keep `selected` in sync so a follow-up call that loses
        // `currentValue` still picks the right value via the fallback.
        for (const v of values) {
            v.selected = v === value;
        }

        if (value.groupId) {
            this.groupId = value.groupId;
        }

        option.groupIds = values.map((v) => v.groupId);
        option.groupIds = option.groupIds.filter(item => item !== undefined);

        if (option.groupIds) {
            // xóa group không kích hoạt
            const needRemoveGroupIds = option.groupIds.filter((groupId) => {
                return groupId != value.groupId;
            });
            if (this.groupIds && Array.isArray(this.groupIds)) {
                this.groupIds = this.groupIds.filter(groupId => !needRemoveGroupIds.includes(groupId));
            }
            // thêm group cần kích hoạt
            if (this.groupIds && !this.groupIds.includes(value.groupId)) {
                this.groupIds.push(value.groupId);
            }

            this.groupIds = this.groupIds.filter(item => item !== undefined);
        }

        let functionItems = option.functionItems;
        if (!functionItems) {
            return;
        }

        for (let functionItem of functionItems) {
            let elementId = functionItem.elementId;
            // find element in template
            let element = this.template.elements.find(
                item => item.elementId == elementId
            );
            if (!element) {
                continue;
            }

            if (functionItem.type == 'dynamic-image') {
                let valueId = this.getValueId(value, option);
                let dynamicImageElement: DynamicImageElement = element as DynamicImageElement;
                if (dynamicImageElement.config.images?.length && ("library" in dynamicImageElement.config)) {
                    let image = dynamicImageElement.config.images?.find(
                        item => item.order == valueId
                    );
                    if (image) {
                        dynamicImageElement.config.imageUrl = image.value;
                        dynamicImageElement.config.imageId = valueId;
                    } else {
                        dynamicImageElement.config.imageUrl = '';
                        dynamicImageElement.config.imageId = valueId;
                    }
                } else {
                    dynamicImageElement.config.imageId = valueId;
                    if (dynamicImageElement.config.images?.length) {
                        let image = dynamicImageElement.config.images?.find(
                            item => item.order == valueId
                        );
                        if (image) {
                            dynamicImageElement.config.imageUrl = image.value;
                        } else {
                            dynamicImageElement.config.imageUrl = '';
                        }
                    }
                }

                // check if element has child elements
                if (dynamicImageElement.childElementIds?.length) {
                    for (let childElementId of dynamicImageElement.childElementIds) {
                        let childElement = this.template.elements.find(
                            item => item.elementId == childElementId
                        ) as DynamicImageElement;

                        if (childElement) {
                            childElement.config.imageId = valueId;
                            childElement.config.imageUrl = dynamicImageElement.config.imageUrl;
                            this.setElement(childElement, childElementId);
                        }
                    }
                }

                this.setElement(dynamicImageElement, elementId);
            } else if (functionItem.type == 'font-type') {
                let valueId = this.getValueId(value, option);
                let textInputElement = element as TextInputElement;
                if (textInputElement.config.fonts) {
                    let font = textInputElement.config.fonts.find(
                        item => item.order == valueId
                    );
                    if (font) {
                        textInputElement.config.textConfig.font = font.value;
                        textInputElement.config.fontId = valueId;
                    }
                } else {
                    textInputElement.config.fontId = valueId;
                }

                // check if element has child elements
                if (textInputElement.childElementIds?.length) {
                    for (let childElementId of textInputElement.childElementIds) {
                        let childElement = this.template.elements.find(
                            item => item.elementId == childElementId
                        ) as TextInputElement;

                        if (childElement) {
                            childElement.config.fontId = valueId;
                            if (textInputElement.config.textConfig.font) {
                                childElement.config.textConfig.font = textInputElement.config.textConfig.font;
                                this.setElement(childElement, childElementId)
                            }

                        }
                    }
                }

                this.setElement(textInputElement, elementId);
            } else if (functionItem.type == 'text-color') {
                let valueId = this.getValueId(value, option);
                let textInputElement = element as TextInputElement;
                if (textInputElement.config.colors) {
                    let color = textInputElement.config.colors.find(
                        item => item.order == valueId
                    );
                    if (color) {
                        textInputElement.config.textConfig.fill = color.value;
                        textInputElement.config.colorId = valueId;
                    }
                } else {
                    textInputElement.config.colorId = valueId;
                }

                // check if element has child elements
                if (textInputElement.childElementIds?.length) {
                    for (let childElementId of textInputElement.childElementIds) {
                        let childElement = this.template.elements.find(
                            item => item.elementId == childElementId
                        ) as TextInputElement;

                        if (childElement) {
                            if (textInputElement.config.textConfig.fill) {
                                childElement.config.textConfig.fill = textInputElement.config.textConfig.fill
                            }

                            childElement.config.colorId = valueId;
                            this.setElement(childElement, childElementId)
                        }
                    }
                }

                this.setElement(textInputElement, elementId);
            } else if (functionItem.type === 'vector') {
                let valueId = this.getValueId(value, option);
                let vectorElement: VectorElement = element as VectorElement;
                if (vectorElement.config.vectors?.length) {
                    let image = vectorElement.config.vectors?.find(
                        item => item.order == valueId
                    );
                    if (image) {
                        vectorElement.config.imageUrl = image.value;
                        vectorElement.config.vectorId = valueId;
                    }
                } else {
                    vectorElement.config.vectorId = valueId;
                }

                // check if element has child elements
                if (vectorElement.childElementIds?.length) {
                    for (let childElementId of vectorElement.childElementIds) {
                        let childElement = this.template.elements.find(
                            item => item.elementId == childElementId
                        ) as DynamicImageElement;

                        if (childElement) {
                            childElement.config.imageId = valueId;
                            if (vectorElement.config.imageUrl) {
                                childElement.config.imageUrl = vectorElement.config.imageUrl;
                                this.setElement(childElement, childElementId);
                            }
                        }
                    }
                }

                this.setElement(element, elementId);
            }
        }
    }

    private changeImageUploadValue(option: ImageUploadOption) :void {
        if (!this.template) {
            return;
        }

        if (!option.fileUploadImageId || typeof option.currentValue === 'undefined') {
            return;
        }

        const imageUrl = option.currentValue.toString();
        let uploadImageElementId = option.fileUploadImageId;
        let uploadImageElement = this.template.elements.find(
            item => item.elementId == uploadImageElementId
        ) as DynamicImageElement | ImagePlaceHolderElement;
        if (uploadImageElement) {
            uploadImageElement.config.imageUrl = imageUrl;
            // check if element has child elements
            if (uploadImageElement.childElementIds?.length) {
                for (let childElementId of uploadImageElement.childElementIds) {
                    let childElement = this.template.elements.find(
                        item => item.elementId == childElementId
                    ) as DynamicImageElement | ImagePlaceHolderElement;

                    if (childElement) {
                        childElement.config.imageUrl = imageUrl;
                        // set element to template.elements
                        let index = this.template.elements.findIndex(
                            item => item.elementId == childElementId
                        );
                        if (index >= 0) {
                            this.template.elements[index] = childElement;
                        }
                    }
                }
            }

            // set element to template.elements
            let index = this.template.elements.findIndex(
                item => item.elementId == uploadImageElementId
            );
            if (index >= 0) {
                this.template.elements[index] = uploadImageElement;
            }
        }
    }

    /**
     * Apply a Date Picker option's selection to every calendar element it
     * targets via `functionItems` of type `calendar`. The selected date is
     * parsed from the option's `currentValue` (ISO `YYYY-MM-DD`) and
     * overlaid onto the element's `calendarConfig` as
     * `selectedDate` / `selectedYear` / `selectedMonth` / `selectedDay` so a
     * downstream renderer can mark the day on the rendered month grid.
     *
     * Pre-existing `calendarConfig` fields (markerType, fonts, colours,
     * etc.) are preserved — only the selection keys are overwritten. When
     * the option has no value, the selection keys are cleared so the grid
     * renders without a marker.
     */
    /**
     * Normalize `calendarConfig` to a plain object. Customily ships it as a
     * JSON-encoded string on some endpoints and as a parsed object on
     * others. Spreading a string with `{...str}` produces a
     * character-indexed dictionary (`{"0":"{","1":"\"","2":"m",...}`), so
     * normalize here before we touch it. Falls back to `{}` when the value
     * is missing, invalid JSON, or some unexpected primitive.
     */
    private parseCalendarConfig(value: any): any {
        if (!value) return {};
        if (typeof value === 'string') {
            try {
                const parsed = JSON.parse(value);
                if (parsed && typeof parsed === 'object') return parsed;
            } catch (e) {
                // Fall through to empty object below.
            }
            return {};
        }
        if (typeof value === 'object') return value;
        return {};
    }

    private changeDatePickerValue(option: DatePickerOption): void {
        if (!this.template) {
            return;
        }

        const functionItems = option.functionItems;
        if (!functionItems || !functionItems.length) {
            return;
        }

        const iso = option.currentValue != null ? String(option.currentValue) : '';
        const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
        const selectedDate = m ? iso : '';
        const selectedYear = m ? parseInt(m[1], 10) : null;
        const selectedMonth = m ? parseInt(m[2], 10) : null;
        const selectedDay = m ? parseInt(m[3], 10) : null;

        for (const functionItem of functionItems) {
            // Restrict to calendar function items. Other function types on a
            // Date Picker option (rare, but possible if Customily ever
            // attaches a secondary effect) shouldn't have their config
            // clobbered with date-picker state.
            if (functionItem.type && functionItem.type !== 'calendar') {
                continue;
            }

            const elementId = functionItem.elementId;
            if (elementId === undefined) {
                continue;
            }
            const element = this.template.elements.find(
                item => item.elementId == elementId
            );
            if (!element) {
                continue;
            }

            const calendarElement = element as CalendarElement;
            const existing = this.parseCalendarConfig(calendarElement.config.calendarConfig);
            calendarElement.config.calendar = true;
            calendarElement.config.calendarConfig = {
                ...existing,
                selectedDate,
                selectedYear,
                selectedMonth,
                selectedDay,
            };

            // Propagate to child elements so multi-page or grouped calendar
            // setups (rare, but present in some Customily templates) stay in
            // sync with the parent's selection.
            if (calendarElement.childElementIds && calendarElement.childElementIds.length) {
                for (const childElementId of calendarElement.childElementIds) {
                    const childElement = this.template.elements.find(
                        item => item.elementId == childElementId
                    ) as CalendarElement | undefined;
                    if (childElement && childElement.config) {
                        const childExisting = this.parseCalendarConfig(childElement.config.calendarConfig);
                        childElement.config.calendar = true;
                        childElement.config.calendarConfig = {
                            ...childExisting,
                            selectedDate,
                            selectedYear,
                            selectedMonth,
                            selectedDay,
                        };
                        this.setElement(childElement, childElementId);
                    }
                }
            }

            this.setElement(calendarElement, elementId);
        }
    }

    private changeTextInputValue(option: TextInputOption): void {
        if (!this.template) {
            return;
        }

        if (option.showSpotifyPlayer) {
            this.changeTrackOption(option);

            return;
        }

        if (typeof option.currentValue === 'undefined') {
            return;
        }

        let text = option.currentValue.toString();
        let functionItems = option.functionItems;
        if (!functionItems) {
            return;
        }

        for (let functionItem of functionItems) {
            let elementId = functionItem.elementId;
            // find element in template
            let element = this.template.elements.find(
                item => item.elementId == elementId
            );
            if (!element) {
                continue;
            }

            if (functionItem.type == 'text') {
                let textInputElement = element as TextInputElement;
                textInputElement.config.textConfig.text = text;

                // check if element has child elements
                if (textInputElement.childElementIds?.length) {
                    for (let childElementId of textInputElement.childElementIds) {
                        let childElement = this.template.elements.find(
                            item => item.elementId == childElementId
                        ) as TextInputElement;

                        if (childElement) {
                            childElement.config.textConfig.text = text;
                            // set element to template.elements
                            this.setElement(childElement, childElementId);
                        }
                    }
                }

                // set element to template.elements
                this.setElement(element, elementId);
            }
        }
    }

    private isChangeTemplateOption(option: SwatchOption | DropdownOption): boolean {
        let functionItems = option.functionItems;
        if (!functionItems) {
            return false;
        }

        for (let functionItem of functionItems) {
            if (functionItem.type == 'change-template') {
                return true;
            }
        }

        return false;
    }

    private getValueId(value: SwatchValue | DropdownValue, option: IOption): number | string {
        let valueId: number | string = -1;
        if ((value.imageId && option.source != 'callie') || (typeof value.imageId != 'undefined' && option.source == 'callie')) {
            valueId = value.imageId;
        } else if (typeof value.id != 'undefined') {
            if (typeof value.id === "string") {
                valueId = parseInt(value.id) + 1;
            } else {
                valueId = value.id + 1;
            }
        } else if (typeof value.value != 'undefined' && isNumeric(value.value)) {
            if (typeof value.value === "string") {
                valueId = parseInt(value.value) + 1;
            } else {
                valueId = value.value + 1;
            }
        }
        return valueId;
    }

    private setElement (element: IElement, elementId: string | number | undefined) {
        // set element to template.elements
        if (typeof elementId !== 'undefined') {
            let index = this.template.elements.findIndex(
                item => item.elementId == elementId
            );
            if (index >= 0) {
                this.template.elements[index] = element;
            }
        }
    }

    private getElement(elements, uid, uidKey = null) {
        if (!uidKey) {
            uidKey = 'uid';
        }
        if (elements != undefined) {
            return elements.find(function (el) {
                return el[uidKey] == uid;
            });
        }
        return undefined;
    }

    private updateVisibleElement() {
        if (!this.template) {
            return;
        }

        this.buildShowHideElements();
        const VISIBILITY = {show: 1, hide: 0};
        const hasGroups = typeof this.groups != 'undefined' && Array.isArray(this.groups) && this.groups.length > 0;
        const allGroupPositionsZero = hasGroups && this.groups.every((group: any) => parseInt(group.position) === 0);
        for (const element of this.template.elements) {
            let opacity = VISIBILITY.hide;
            element.isShow = false;

            if (this.showElementIds.includes(element.elementId) || this.showElementIds.includes(element.elementId.toString())) {
                opacity = VISIBILITY.show;
                element.isShow = true;
            } else if (!this.hideElementIds.includes(element.elementId) && !this.hideElementIds.includes(element.elementId.toString())) {
                opacity = VISIBILITY.show;
            }

            // check show hide by group
            if (!allGroupPositionsZero && element.groupId && typeof this.selectedGroups != 'undefined') {
                const groupIds = this.selectedGroups.map((g) => {
                    return g.id
                });

                if (!groupIds.includes(element.groupId)) {
                    opacity = VISIBILITY.hide;
                    element.isShow = false;
                }
            }

            element.opacity = opacity;

            if (element && element.childElementIds && element.childElementIds.length) {
                element.childElementIds.forEach(childElementId => {
                    const childElement = this.getElement(this.template?.elements, childElementId, 'element_id');
                    if (childElement) {
                        const isFromCallieElement = element.source == 'callie';
                        let isValidChildElement = true;
                        if (isFromCallieElement) {
                            // find option by id
                            const option = this.options.find(function(option) {
                                return option.id == childElementId;
                            });
                            const baseOption = this.options.find(function(option) {
                                return option.id == element.elementId;
                            });
                            if (option && baseOption) {
                                isValidChildElement = option.isShow && !option.hideVisually && !option.isCallieHide;
                                const isValidShowBaseElement = baseOption.isShow && !baseOption.hideVisually && !baseOption.isCallieHide;
                                if (!isValidChildElement && isValidShowBaseElement) {
                                    childElement.isShow = false;
                                    childElement.opacity = 0;
                                    this.setElement(childElement, childElementId);
                                }
                            }
                        }
                    }
                });
            }
        }
    }

    private buildShowHideElements() {
        let showElementIds: (string | number)[] = [];
        let hideElementIds: (string | number)[] = [];
        this.setSelectedGroups();
        for (const thisOption of this.options) {
            // check show / hide on canvas
            // upload image id
            if ("fileUploadImageId" in thisOption && thisOption.fileUploadImageId) {

                const element = this.template.elements.find(function(element: IElement) {
                    return element.elementId == thisOption.fileUploadImageId;
                });

                if (element) {
                    this.addShowConditionArray(element, thisOption, showElementIds, hideElementIds);
                }
            }

            // function items
            if (thisOption.functionItems && thisOption.functionItems.length) {
                // function items
                for (let j = 0; j < thisOption.functionItems.length; j++) {
                    const elementId = thisOption.functionItems[j].elementId;
                    const element = this.template.elements.find(function(element) {
                        return element.elementId == elementId;
                    });

                    if (element) {
                        if (thisOption.functionItems[j].type == 'text' && element.type == 'dynamic_image') continue;
                        this.addShowConditionArray(element, thisOption, showElementIds, hideElementIds);
                    }
                }
            }
        }

        // convert to unique array
        showElementIds = showElementIds.filter(function (value, index, self) {
            return self.indexOf(value) === index;
        });
        hideElementIds = hideElementIds.filter(function (value, index, self) {
            return self.indexOf(value) === index;
        });

        this.showElementIds = showElementIds;
        this.hideElementIds = hideElementIds;
    }

    private setSelectedGroups() {
        const groups = [];
        if (typeof this.groups != 'undefined' && this.groups.length) {
            const parentGroup = this.groups.find(group =>{
                return group.parentId === null && this.groupIds.includes(group.position.toString())
            });
            const parentGroupId = parentGroup?.id;
            for (let i = 0; i < this.groups.length; i++) {
                const group = this.groups[i];
                for (let j = 0; j < this.groupIds.length; j++) {
                    const positionMatch = parseInt(this.groupIds[j]) === parseInt(group.position);

                    const parentMatch = !parentGroupId ||
                        group.parentId === null ||
                        group.parentId === parentGroupId;

                    if (positionMatch && parentMatch) {
                        groups.push(group);
                    }
                }
            }
        }

        if (groups.length) {
            this.selectedGroups = groups;
        }
    }

    private addShowConditionArray = (element: IElement, option: IOption, showElementIds: (string | number)[], hideElementIds: (string | number)[]) => {
        const childElementIds = element.childElementIds ?? [];
        let emptyValues = true;
        if ("swatchValues" in option) {
            emptyValues = (option.swatchValues && !option.swatchValues.length);
        }

        if ("dropdownValues" in option) {
            emptyValues = (option.dropdownValues && !option.dropdownValues.length);
        }

        if (option.type == 'text-input') emptyValues = false;
        if (option.type == 'image-upload') emptyValues = false;
        if (option.type == 'date-picker') emptyValues = false;
        if ((option.isShow || typeof option.hideVisually == 'undefined') && !showElementIds.includes(element.elementId)  && !emptyValues) {
            showElementIds.push(element.elementId);
            if (childElementIds && childElementIds.length) {
                for (let k = 0; k < childElementIds.length; k++) {
                    showElementIds.push(childElementIds[k]);
                }
            }
        } else {
            hideElementIds.push(element.elementId);
            if (childElementIds && childElementIds.length) {
                for (let k = 0; k < childElementIds.length; k++) {
                    hideElementIds.push(childElementIds[k]);
                }
            }
        }
    }

    getFabricConfig(element: IElement) {
        switch (element.type) {
            case 'text_box_circular':
                return getRenderConfigTextBoxCircular(element as CircularTextInputElement);
            case 'text_box':
                return getRenderConfigTextBox(element as TextInputElement);
            case 'dynamic_image':
            case 'image_placeholder':
                return getRenderConfigImage(element as DynamicImageElement);
            case 'vector_eps':
                return getRenderConfigVectorEps(element as VectorElement);
        }
    }

    setColorGuideImageUrl(imageUrl: string) {
        this.colorGuideImageUrl = imageUrl;
    }

    async replaceMockup() {
        if (!this.isAllowReplace() || !this.colorGuideImageUrl) {
            return;
        }

        const ratio = this.getBackgroundRatio();
        if (this.template.baseFile && this.isImageExtURL(this.template.baseFile)) {
            this.template.baseFileWidth = this.template.width * ratio;
            this.template.baseFileHeight = this.template.height * ratio;
            this.template.baseFile = this.colorGuideImageUrl;

            return;
        }

        const elements = this.template.elements;
        for (const element of elements) {
            if (await this.isMockupElement(element)) {
                element.config.sWidth = element.config.sWidth * ratio;
                element.config.sHeight = element.config.sHeight * ratio;
                element.config.imageUrl = this.colorGuideImageUrl;
                this.setElement(element, element.elementId);
            }
        }
    }

    private getReplaceMockupConfig() {
        return {
            allowReplaceBackground: true,
            allowReplaceBackgroundPrefix: [
                't-shirts',
                'sweatshirts',
                'baseball-tee',
                'hoodies',
                'long-sleeves'
            ],
            backgroundRatio: {
                slugs: {
                    "women-classic-t-shirt": 1.3,
                    "baseball-tee": 1.6,
                    "sweatshirt": 1.3,
                    "hoodies": 1.7,
                    "long-sleeve": 1.3,
                },
                default: 1.1
            }
        }
    }

    private getBackgroundRatio() {
        let config = this.getReplaceMockupConfig();
        for (let slug of Object.keys(config.backgroundRatio.slugs)) {
            if (this.colorGuideImageUrl.includes(slug)) {
                // @ts-ignore
                return config.backgroundRatio.slugs[slug];
            }
        }

        return config.backgroundRatio.default;
    }

    private isAllowReplace() {
        if (!this.isAllowReplaceBackground) {
            return false;
        }

        const imagePath = this.colorGuideImageUrl.replace('https://cdn.printerval.com/', '');
        let config = this.getReplaceMockupConfig();
        let slug = imagePath.split('/').pop();
        for (let i = 0; i < config.allowReplaceBackgroundPrefix.length; i++) {
            if (slug?.toLowerCase().indexOf(config.allowReplaceBackgroundPrefix[i].toLowerCase()) == 0) {
                return true;
            }
        }

        return false;
    }

    private async isMockupElement(element: IElement) {
        const ceilWidth = Math.ceil(element.config.sWidth);
        const ceilHeight = Math.ceil(element.config.sHeight);
        let retValue = false;
        if (ceilHeight >= this.template.height && ceilWidth >= this.template.width) {
            retValue = element.config.imageUrl.toLowerCase().split('?')[0].endsWith("jpg");

            if (!retValue) {
                retValue = !(await this.isTransparencyImage(element.config.imageUrl, 100));
            }
        }

        return retValue;
    }

    private async isTransparencyImage(imageUrl: string, threshold = 255) {
        try {
            const urlService = 'https://printerval.com/module/customization/media/check-transparent';
        
            const response = await axios.get(urlService + '?url=' + imageUrl);

            return !!(response.data && response.data.is_tranparent);
        } catch (error) {
            console.error('onerror', imageUrl, error);
            return false;
        }
    }

    private isImageExtURL(url: string) {
        const cleanURL = url.split('?')[0];
        return (cleanURL.match(/\.(jpeg|jpg|gif|png|bmp|webp)$/i) != null);
    }

    private initImages(options: ImageOptions) {
        return new Images(options);
    }

    setInputSearchSong(inputSearchSong) {
        this.inputSearchSong = inputSearchSong;
    }

    async _openSearchSpotifySong(targetOption) {
        targetOption.showSpotifyPlayer = true;
        if (!this.spotifyToken.access_token || this.spotifyToken.expires_in <= Date.now() / 1000) {
            await this.getSpotifyToken();
        }
    }

    async getSpotifyToken() {
        const data = "grant_type=client_credentials&client_id=3d9813c912a0442ea48cade439f76cdf&client_secret=f19dd32798ae4e10b6d4011895086227";

        const response = await axios.post("https://accounts.spotify.com/api/token", data, {
            headers: {
                "Content-Type": "application/x-www-form-urlencoded"
            }
        });

        const spotifyTokenData = response.data;
        if (spotifyTokenData) {
            this.spotifyToken = {
                access_token: spotifyTokenData.access_token,
                expires_in: (Date.now() / 1000) + spotifyTokenData.expires_in
            };
        }
    }

    getArtistsName(artists) {
        try {
            return artists.map((artist) => {
                return artist.name;
            }).join(', ');
        } catch (error) {
            return '';
        }
    }

    async openSearchSpotifySong(targetOption) {
        if (targetOption.functionItems) {
            for (let i = 0; i < targetOption.functionItems.length; i++) {
                const functionItem = targetOption.functionItems[i];
                if (functionItem.type == 'spotify-player') {
                    return await this._openSearchSpotifySong(targetOption);
                }
            }
        }
    }

    async searchSong() {
        const context = this;
        const url = 'https://api.spotify.com/v1/search?type=track&q=' + context.inputSearchSong + ' ';
        const response = await axios.get(url, {
            headers: {
                Authorization: 'Bearer ' + context.spotifyToken.access_token
            }
        });
        const data = response.data;
        if (data.tracks && data.tracks.items.length) {
            context.searchSongResult = data.tracks.items;
        }
    }

    changeTrackOption(option: TextInputOption){
        this.searchSongResult = [];
        this.inputSearchSong = option.track.name + ' - ' + this.getArtistsName(option.track.artists);
        option.showSpotifyPlayer = false;
        option.currentValue = this.inputSearchSong;
        const optionIndex = this.options.findIndex(o => o.id == option.id);
        if (optionIndex < 0) {
            return;
        }

        this.options[optionIndex] = option;
        const groupIds = [];
        for (let i = 0; i < option.functionItems.length; i++) {
            const functionItem = option.functionItems[i];
            if (functionItem.type == 'spotify-player') {
                const groupUuid = functionItem.groupUuid;
                const group = this.groups.find((gr) => {
                    return gr.uuid == groupUuid;
                });

                if (group) {
                    groupIds.push(group.id);
                    option.groupId = group.position;
                    this.groupIds.push(group.position);

                    const groupElements = this.template.elements.filter(function(element) {
                        return element.groupId == group.id;
                    })

                    let isRenderName = false;
                    for (let j = 0; j < groupElements.length; j++)
                    {
                        const element = groupElements[j];
                        if (element.type != 'text_box') continue;

                        if (!isRenderName) {
                            element.config.textConfig.text = option.track.name;
                            isRenderName = true;
                        } else {
                            element.config.textConfig.text = this.getArtistsName(option.track.artists);
                        }

                        this.setElement(element, element.elementId);
                    }
                }
            }
        }
    }
}

export default Customization;
