// (C) 2019-2026 GoodData Corporation
import { cloneDeep, isEqual } from "lodash-es";
import { insightHasDataDefined, insightProperties, } from "@gooddata/sdk-model";
import { DefaultLocale, UnexpectedSdkError, isClusteringNotReceived, isForecastNotReceived, isGoodDataSdkError, } from "@gooddata/sdk-ui";
import { EmptyAfmSdkError, isEmptyAfm, } from "../../interfaces/Visualization.js";
import { findDerivedBucketItem, hasDerivedBucketItems, isDerivedBucketItem, } from "../../utils/bucketHelper.js";
import { createInternalIntl } from "../../utils/internalIntlProvider.js";
import { getSupportedProperties } from "../../utils/propertiesHelper.js";
export class AbstractPluggableVisualization {
    intl;
    locale;
    featureFlags;
    permissions;
    /**
     * Standard callback
     */
    callbacks;
    /**
     * Insight that is currently rendered by the pluggable visualization. This field is set during
     * every call to {@link update} and will remain the same until the next update() call.
     */
    currentInsight;
    currentOptions;
    visualizationProperties;
    supportedPropertiesList;
    propertiesMeta;
    /**
     * List of properties which affect content of reference point and when these changed, reference point needs to be re-generated
     *
     * Note: Deep object value comparison is being used.
     */
    propertiesAffectingReferencePoint;
    /**
     * Classname or a getter function of the element where visualization should be mounted
     */
    element;
    /**
     * Classname or a getter of the element where config panel should be mounted
     */
    configPanelElement;
    hasError;
    hasEmptyAfm;
    isLoading;
    getIsError = () => {
        return this.hasEmptyAfm || this.hasError;
    };
    constructor(props) {
        this.callbacks = props.callbacks;
        this.locale = props.locale ?? DefaultLocale;
        this.intl = createInternalIntl(this.locale, props.messages);
        this.element = props.element;
        this.configPanelElement = props.configPanelElement;
        this.featureFlags = props.featureFlags ?? {};
        this.permissions = props.permissions;
        this.propertiesAffectingReferencePoint = [];
    }
    /**
     * Get an element where the visualization should be mounted
     */
    getElement() {
        if (typeof this.element === "function") {
            return this.element();
        }
        return document.querySelector(this.element);
    }
    /**
     * Get an element where the config panel should be mounted
     */
    getConfigPanelElement() {
        if (typeof this.configPanelElement === "function") {
            return this.configPanelElement();
        }
        return document.querySelector(this.configPanelElement);
    }
    //
    // Templated implementation of update contract
    //
    /**
     * Templated implementation of the update method. Given options, insight to render and the execution
     * factory, this function will drive the update process. It consists of the following:
     *
     * 1. call to {@link updateInstanceProperties} - this method should update any internal state
     *    of the instance's properties. Subclasses MAY override this to update state of their own private
     *    properties.
     *
     * 2. call to {@link checkBeforeRender} - this method is called as a hook to perform final check before
     *    the actual rendering is triggered:
     *    - if hook returns true, vis will be rendered
     *    - if hook returns false, vis will not be rendered
     *    - if hook throws an exception, it will be sent via onError callback; vis will not be rendered
     *
     * 3. vis rendering is triggered (unless step 2 determines it should not be)
     *
     * 4. configuration panel is rendered (always)
     *
     * Note: do not override this method.
     */
    update(options, insight, insightPropertiesMeta, executionFactory) {
        this.updateInstanceProperties(options, insight, insightPropertiesMeta);
        this.hasEmptyAfm = !insightHasDataDefined(insight);
        let shouldRenderVisualization;
        try {
            shouldRenderVisualization = this.checkBeforeRender(insight);
        }
        catch (e) {
            const sdkError = isGoodDataSdkError(e)
                ? e
                : new UnexpectedSdkError(undefined, e instanceof Error ? e : new Error(String(e)));
            this.onError(sdkError);
            return;
        }
        if (shouldRenderVisualization) {
            this.renderVisualization(options, insight, executionFactory);
        }
        this.renderConfigurationPanel(insight, options);
    }
    /**
     * This method will be called during the {@link update} processing. This is where internal properties of the
     * concrete plug vis class MAY be updated. If class overrides this method, it MUST call the method in
     * superclass.
     *
     * @param options - visualization options
     * @param insight - insight that is about to be rendered
     */
    updateInstanceProperties(options, insight, insightPropertiesMeta) {
        this.visualizationProperties = getSupportedProperties(insightProperties(insight), this.supportedPropertiesList);
        this.propertiesMeta = insightPropertiesMeta ?? null;
        this.currentInsight = insight;
        this.currentOptions = options;
    }
    /**
     * This method will be called during the {@link update} processing. It can be used to influence whether
     * visualization should be rendered and optionally whether particular error should be rendered by the app.
     *
     * @param insight - insight that is about to be rendered
     * @returns when true is returned (default), visualization will be rendered, when false is returned no rendering is done
     * @throws error - if anything is thrown, visualization will not be rendered and the exception will be passed via onError callback
     */
    checkBeforeRender(insight) {
        if (!insightHasDataDefined(insight)) {
            throw new EmptyAfmSdkError();
        }
        return true;
    }
    //
    // Callback delegates
    //
    onError = (error) => {
        this.callbacks.onError?.(error);
        // EMPTY_AFM is handled in update as it can change on any render contrary to other error types
        // that have to be set manually or by loading
        // FORECAST_NOT_RECEIVED is not handled as an normal error, cause its not results in invalid chart
        if (!isEmptyAfm(error) && !isForecastNotReceived(error) && !isClusteringNotReceived(error)) {
            this.hasError = true;
        }
        this.renderConfigurationPanel(this.currentInsight, this.currentOptions);
    };
    onLoadingChanged = (loadingState) => {
        this.callbacks.onLoadingChanged?.(loadingState);
        this.hasError = false;
        this.isLoading = loadingState.isLoading;
        this.renderConfigurationPanel(this.currentInsight, this.currentOptions);
    };
    onExportReady = (exportResult) => {
        this.callbacks.onExportReady?.(exportResult);
    };
    pushData = (data, options) => {
        this.callbacks.pushData?.(data, options);
    };
    afterRender = () => {
        this.callbacks.afterRender?.();
    };
    onDrill = (event) => {
        // in case onDrill is not specified, default to always firing drill events
        return this.callbacks.onDrill ? this.callbacks.onDrill(event) : true;
    };
    onDataView = (dataView) => {
        this.callbacks.onDataView?.(dataView);
    };
    //
    // Templated implementation of addNewDerivedBucketItems contract
    //
    addNewDerivedBucketItems(referencePoint, newDerivedBucketItems) {
        if (!referencePoint.buckets) {
            return Promise.resolve(referencePoint);
        }
        const newReferencePoint = cloneDeep(referencePoint);
        newReferencePoint.buckets = referencePoint.buckets.map((bucket) => {
            return {
                ...bucket,
                items: this.mergeDerivedBucketItems(referencePoint, bucket, newDerivedBucketItems),
            };
        });
        return Promise.resolve(newReferencePoint);
    }
    mergeDerivedBucketItems(referencePoint, bucket, newDerivedBucketItems) {
        return bucket.items.reduce((resultItems, bucketItem) => {
            const newDerivedBucketItem = findDerivedBucketItem(bucketItem, newDerivedBucketItems);
            const shouldAddItem = newDerivedBucketItem &&
                !isDerivedBucketItem(bucketItem) &&
                !hasDerivedBucketItems(bucketItem, referencePoint.buckets);
            if (shouldAddItem) {
                resultItems.push(newDerivedBucketItem);
            }
            resultItems.push(bucketItem);
            return resultItems;
        }, []);
    }
    /**
     * Default no-op implementation of the drill down, which just returns the original visualization.
     *
     * @param sourceVisualization - drill down source {@link IInsight}
     * @param _drillDownContext - drill context (unused in this implementation)
     * @param _backendSupportsElementUris - whether backend supports elements by uri (unused in this implementation)
     * @returns the `sourceVisualization`
     * @see {@link IVisualization.getInsightWithDrillDownApplied} for more information
     */
    getInsightWithDrillDownApplied(sourceVisualization, _drillDownContext, _backendSupportsElementUris) {
        return sourceVisualization;
    }
    /**
     * Default implementation of sort config getter returning empty object
     *
     * @param _referencePoint - reference point
     * @returns promise promise once resolved returns an sort config
     */
    getSortConfig(_referencePoint) {
        return Promise.resolve({
            defaultSort: [],
            availableSorts: [],
            supported: false,
            disabled: false,
        });
    }
    haveSomePropertiesRelevantForReferencePointChanged(currentReferencePoint, nextReferencePoint) {
        return (this.propertiesAffectingReferencePoint.some((prop) => !isEqual(currentReferencePoint?.properties?.controls?.[prop], nextReferencePoint?.properties?.controls?.[prop])) ||
            // Analytical Designer takes sorts from sortItems and put them to target insight,
            // however there is no visualization update/re-render when they are changed by the visualization itself, and not by Analytical Designer UI.
            // I know this is not nice, but I didn't found any better way how to resolve it.
            !isEqual(currentReferencePoint?.properties?.sortItems ?? [], nextReferencePoint?.properties?.sortItems ?? []));
    }
    getBucketsToUpdate(_currentReferencePoint, _nextReferencePoint) {
        return undefined;
    }
}
//# sourceMappingURL=AbstractPluggableVisualization.js.map