// (C) 2019-2020 GoodData Corporation
import set = require("lodash/set");
import get = require("lodash/get");
import uniq = require("lodash/uniq");
import uniqBy = require("lodash/uniqBy");
import negate = require("lodash/negate");
import includes = require("lodash/includes");
import every = require("lodash/every");
import forEach = require("lodash/forEach");
import cloneDeep = require("lodash/cloneDeep");
import isEmpty = require("lodash/isEmpty");
import flatMap = require("lodash/flatMap");
import compact = require("lodash/compact");
import without = require("lodash/without");
import { IntlShape } from "react-intl";
import { VisType, VisualizationTypes } from "../../constants/visualizationTypes";
import * as BucketNames from "../../constants/bucketNames";
import { OverTimeComparisonType, OverTimeComparisonTypes } from "../../interfaces/OverTimeComparison";
import { Execution, VisualizationObject } from "@gooddata/typings";

import {
    IFiltersBucketItem,
    IBucketItem,
    IBucket,
    IExtendedReferencePoint,
    IUiConfig,
    IBucketsUiConfig,
    IBucketUiConfig,
    IFilters,
    IBucketFilter,
    IDateFilter,
    isDateFilter,
    isAttributeFilter,
    isMeasureValueFilter,
} from "../interfaces/Visualization";
import {
    DATE_DATASET_ATTRIBUTE,
    ATTRIBUTE,
    DATE,
    METRIC,
    BUCKETS,
    SHOW_ON_SECONDARY_AXIS,
} from "../constants/bucket";
import { UICONFIG } from "../constants/uiConfig";
import { getTranslation } from "./translations";
import { filterOutEmptyBuckets } from "../../helpers/mdObjBucketHelper";
import { SUPPORTED_MEASURE_BUCKETS } from "../../components/visualizations/chart/chartOptions/bulletChartOptions";

export function sanitizeFilters(newReferencePoint: IExtendedReferencePoint): IExtendedReferencePoint {
    const attributeBucketItems = getAllAttributeItems(newReferencePoint.buckets);
    const measureBucketItems = getAllMeasureItems(newReferencePoint.buckets);

    newReferencePoint.filters = newReferencePoint.filters || {
        localIdentifier: "filters",
        items: [],
    };

    const filteredFilters = newReferencePoint.filters.items.filter((filterBucketItem: IFiltersBucketItem) => {
        const filter = filterBucketItem.filters[0];

        if (isAttributeFilter(filter) || isDateFilter(filter)) {
            if (filterBucketItem.autoCreated === false) {
                return true;
            }
            return attributeBucketItems.some(
                (attributeBucketItem: IBucketItem) => attributeBucketItem.attribute === filter.attribute,
            );
        } else if (isMeasureValueFilter(filter)) {
            if (attributeBucketItems.length === 0) {
                return false;
            }
            return measureBucketItems.some(
                (measureBucketItem: IBucketItem) =>
                    measureBucketItem.localIdentifier === filter.measureLocalIdentifier,
            );
        }

        return false;
    });

    return {
        ...newReferencePoint,
        filters: {
            ...newReferencePoint.filters,
            items: filteredFilters,
        },
    };
}

export function isDerivedBucketItem(measureItem: IBucketItem): boolean {
    return !!measureItem.masterLocalIdentifier;
}

function isArithmeticBucketItem(bucketItem: IBucketItem): boolean {
    return !!bucketItem.operandLocalIdentifiers;
}

function isDerivedOfTypeBucketItem(measureItem: IBucketItem, derivedType: OverTimeComparisonType): boolean {
    if (!isDerivedBucketItem(measureItem)) {
        return false;
    }

    return measureItem.overTimeComparisonType === derivedType;
}

function findDerivedTypesReferencedByArithmeticMeasure(
    measure: IBucketItem,
    allMeasures: IBucketItem[],
    visitedMeasures: Set<string>,
): Set<OverTimeComparisonType> {
    return measure.operandLocalIdentifiers.reduce(
        (types: Set<OverTimeComparisonType>, operandIdentifier: string) => {
            if (operandIdentifier === null || visitedMeasures.has(operandIdentifier)) {
                return types;
            }
            const operand: IBucketItem = findMeasureByLocalIdentifier(operandIdentifier, allMeasures);
            if (operand === undefined) {
                return types;
            }
            if (isArithmeticBucketItem(operand)) {
                visitedMeasures.add(operandIdentifier);
                findDerivedTypesReferencedByArithmeticMeasure(operand, allMeasures, visitedMeasures).forEach(
                    (type: OverTimeComparisonType) => types.add(type),
                );
            } else if (isDerivedBucketItem(operand) && !types.has(operand.overTimeComparisonType)) {
                types.add(operand.overTimeComparisonType);
            }
            return types;
        },
        new Set(),
    );
}

/**
 * Get array of unique over time comparison types used in ancestors of the provided arithmetic measure.
 *
 * @param measure - the (possibly) arithmetic measure
 * @param buckets - all buckets
 * @return empty array if there are no derived measures in the arithmetic measure ancestors, empty array if provided
 * measure is not arithmetic, array of unique {OverTimeComparisonType} of derived ancestor measures found in arithmetic
 * measure tree.
 */
export function getDerivedTypesFromArithmeticMeasure(
    measure: IBucketItem,
    buckets: IBucket[],
): OverTimeComparisonType[] {
    if (!isArithmeticBucketItem(measure)) {
        return [];
    }

    const allMeasures = flatMap<IBucket, IBucketItem>(buckets, bucket => bucket.items);
    const overTimeComparisonTypes = findDerivedTypesReferencedByArithmeticMeasure(
        measure,
        allMeasures,
        new Set(),
    );
    return Array.from(overTimeComparisonTypes);
}

export function filterOutDerivedMeasures(measures: IBucketItem[]): IBucketItem[] {
    return measures.filter(measure => !isDerivedBucketItem(measure));
}

function isArithmeticMeasureFromDerived(measure: IBucketItem, buckets: IBucket[]): boolean {
    return getDerivedTypesFromArithmeticMeasure(measure, buckets).length > 0;
}

export function filterOutArithmeticMeasuresFromDerived(
    measures: IBucketItem[],
    buckets: IBucket[],
): IBucketItem[] {
    return measures.filter(measure => !isArithmeticMeasureFromDerived(measure, buckets));
}

function isArithmeticMeasureFromDerivedOfTypeOnly(
    measure: IBucketItem,
    buckets: IBucket[],
    derivedType: OverTimeComparisonType,
): boolean {
    const arithmeticMeasureDerivedTypes = getDerivedTypesFromArithmeticMeasure(measure, buckets);
    return arithmeticMeasureDerivedTypes.length === 1 && arithmeticMeasureDerivedTypes[0] === derivedType;
}

export function keepOnlyMasterAndDerivedMeasuresOfType(
    measures: IBucketItem[],
    derivedType: OverTimeComparisonType,
): IBucketItem[] {
    return measures.filter(
        measure => !isDerivedBucketItem(measure) || isDerivedOfTypeBucketItem(measure, derivedType),
    );
}

export function filterOutIncompatibleArithmeticMeasures(
    measures: IBucketItem[],
    buckets: IBucket[],
    derivedOfTypeToKeep: OverTimeComparisonType,
): IBucketItem[] {
    return measures.filter(
        (measure: IBucketItem) =>
            !isArithmeticBucketItem(measure) ||
            !isArithmeticMeasureFromDerived(measure, buckets) ||
            isArithmeticMeasureFromDerivedOfTypeOnly(measure, buckets, derivedOfTypeToKeep),
    );
}

export function isDateBucketItem(bucketItem: IBucketItem): boolean {
    return !!bucketItem && bucketItem.attribute === DATE_DATASET_ATTRIBUTE;
}

export const isNotDateBucketItem = negate(isDateBucketItem);

export function getDateFilter(filtersBucket: IFilters): IDateFilter {
    const dateFiltersInclEmpty = flatMap(filtersBucket.items, filterItem => {
        const filters = get<IFiltersBucketItem, "filters", IBucketFilter[]>(filterItem, "filters", []);
        return filters.find(isDateFilter);
    });
    const dateFilters = compact(dateFiltersInclEmpty);
    return dateFilters.length ? dateFilters[0] : null;
}

export function getComparisonTypeFromFilters(filtersBucket: IFilters): OverTimeComparisonType {
    if (isEmpty(filtersBucket)) {
        return OverTimeComparisonTypes.NOTHING;
    }
    const dateFilter = getDateFilter(filtersBucket);

    return !isEmpty(dateFilter) && dateFilter.overTimeComparisonType
        ? dateFilter.overTimeComparisonType
        : OverTimeComparisonTypes.NOTHING;
}

function bucketSupportsSubtitle(visualizationType: string, bucketLocalIdentifier: string) {
    if (visualizationType === VisualizationTypes.HEADLINE) {
        return true;
    }

    if (visualizationType === VisualizationTypes.SCATTER) {
        return bucketLocalIdentifier !== BucketNames.ATTRIBUTE;
    }

    if (visualizationType === VisualizationTypes.BUBBLE) {
        return bucketLocalIdentifier !== BucketNames.VIEW;
    }

    if (visualizationType === VisualizationTypes.COMBO) {
        return bucketLocalIdentifier !== BucketNames.VIEW;
    }

    if (visualizationType === VisualizationTypes.PUSHPIN) {
        return (
            bucketLocalIdentifier !== BucketNames.LOCATION && bucketLocalIdentifier !== BucketNames.SEGMENT
        );
    }

    if (visualizationType === VisualizationTypes.BULLET) {
        return bucketLocalIdentifier !== BucketNames.VIEW;
    }

    return false;
}

export function setBucketTitles(
    referencePoint: IExtendedReferencePoint,
    visualizationType: string,
    intl?: IntlShape,
): IUiConfig {
    const buckets: IBucket[] = get(referencePoint, BUCKETS);
    const updatedUiConfig: IUiConfig = cloneDeep(get(referencePoint, UICONFIG));

    forEach(buckets, (bucket: IBucket) => {
        const localIdentifier: string = get(bucket, "localIdentifier", "");
        // skip disabled buckets
        if (!get(updatedUiConfig, [BUCKETS, localIdentifier, "enabled"], false)) {
            return;
        }

        if (bucketSupportsSubtitle(visualizationType, localIdentifier)) {
            const subtitleId = generateBucketSubtitleId(localIdentifier, visualizationType);
            const subtitle = getTranslation(subtitleId, intl);
            set(updatedUiConfig, [BUCKETS, localIdentifier, "subtitle"], subtitle);
        }

        const titleId = generateBucketTitleId(localIdentifier, visualizationType);
        const title = getTranslation(titleId, intl);
        set(updatedUiConfig, [BUCKETS, localIdentifier, "title"], title);
    });

    return updatedUiConfig;
}

export function generateBucketTitleId(localIdentifier: string, visualizationType: string): string {
    return `dashboard.bucket.${localIdentifier}_title.${visualizationType}`;
}

export function generateBucketSubtitleId(localIdentifier: string, visualizationType: string): string {
    return `dashboard.bucket.${localIdentifier}_subtitle.${visualizationType}`;
}

export function getItemsCount(buckets: IBucket[], localIdentifier: string): number {
    return getBucketItems(buckets, localIdentifier).length;
}

export function getBucketItems(buckets: IBucket[], localIdentifier: string): IBucketItem[] {
    return get(buckets.find(bucket => bucket.localIdentifier === localIdentifier), "items", []);
}

// return bucket items matching localIdentifiers from any bucket
export function getItemsFromBuckets(
    buckets: IBucket[],
    localIdentifiers: string[],
    types?: string[],
): IBucketItem[] {
    return localIdentifiers.reduce(
        (bucketItems, localIdentifier) =>
            bucketItems.concat(
                types
                    ? getBucketItemsByType(buckets, localIdentifier, types)
                    : getBucketItems(buckets, localIdentifier),
            ),
        [],
    );
}

export function getBucketItemsByType(
    buckets: IBucket[],
    localIdentifier: string,
    types: string[],
): IBucketItem[] {
    const itemsOfType: IBucketItem[] = [];
    const bucketItems: IBucketItem[] = getBucketItems(buckets, localIdentifier);

    bucketItems.forEach((item: IBucketItem) => {
        if (includes(types, item.type)) {
            itemsOfType.push(item);
        }
    });
    return itemsOfType;
}

export function getPreferredBucketItems(
    buckets: IBucket[],
    preference: string[],
    type: string[],
): IBucketItem[] {
    const bucket = getPreferredBucket(buckets, preference, type);
    return get(bucket, "items", []);
}

export function getPreferredBucket(buckets: IBucket[], preference: string[], type: string[]): IBucket {
    return preference.reduce((result: IBucket, preference: string) => {
        if (result) {
            return result;
        }

        return buckets.find((bucket: IBucket) => {
            const preferenceMatch = bucket.localIdentifier === preference;
            const typeMatch = every(get(bucket, "items", []), item => type.indexOf(item.type) !== -1);

            return preferenceMatch && typeMatch;
        });
    }, undefined);
}

export function getAllBucketItemsByType(bucket: IBucket, types: string[]): IBucketItem[] {
    return bucket.items.reduce((resultItems: IBucketItem[], item: IBucketItem): IBucketItem[] => {
        if (includes(types, item.type)) {
            resultItems.push(item);
        }
        return resultItems;
    }, []);
}

export function getAllItemsByType(buckets: IBucket[], types: string[]): IBucketItem[] {
    return buckets.reduce(
        (items: IBucketItem[], bucket: IBucket) => [...items, ...getAllBucketItemsByType(bucket, types)],
        [],
    );
}

export function removeDuplicateBucketItems(buckets: IBucket[]): IBucket[] {
    const usedIdentifiersMap: { [key: string]: boolean } = {};

    return buckets.map(bucket => {
        const filteredBucketItems = bucket.items.filter(bucketItem => {
            const isDuplicate = usedIdentifiersMap[bucketItem.localIdentifier];
            usedIdentifiersMap[bucketItem.localIdentifier] = true;
            return !isDuplicate;
        });
        return filteredBucketItems.length === bucket.items.length
            ? bucket
            : {
                  ...bucket,
                  items: filteredBucketItems,
              };
    });
}

export function getTotalsFromBucket(
    buckets: IBucket[],
    bucketName: string,
): VisualizationObject.IVisualizationTotal[] {
    const selectedBucket = buckets.find(bucket => bucket.localIdentifier === bucketName);
    return get(selectedBucket, "totals", []);
}

export function getUniqueAttributes(buckets: IBucket[]) {
    const attributes = getAllItemsByType(buckets, [ATTRIBUTE, DATE]);
    return uniqBy(attributes, attribute => get(attribute, "attribute"));
}

export function getMeasures(buckets: IBucket[]) {
    return getAllItemsByType(buckets, [METRIC]);
}

export function getFirstValidMeasure(buckets: IBucket[]): IBucketItem {
    const measures = getMeasures(buckets);
    const validMeasures = measures.filter(isValidMeasure);
    return validMeasures[0] || null;
}

function isValidMeasure(measure: IBucketItem): boolean {
    if (isArithmeticBucketItem(measure)) {
        return measure.operandLocalIdentifiers.every(
            operandLocalIdentifier => operandLocalIdentifier !== null,
        );
    }
    return true;
}

export function getFirstAttribute(buckets: IBucket[]): IBucketItem {
    return getUniqueAttributes(buckets)[0] || null;
}

export function getMeasureItems(buckets: IBucket[]): IBucketItem[] {
    const preference = [BucketNames.MEASURES, BucketNames.SECONDARY_MEASURES, BucketNames.TERTIARY_MEASURES];
    const preferredMeasures = preference.reduce((acc, pref) => {
        const prefBucketItems = getPreferredBucketItems(buckets, [pref], [METRIC]);
        return [...acc, ...prefBucketItems];
    }, []);

    // if not found in prefered bucket use all available measure items
    if (isEmpty(get(preferredMeasures, "items", []))) {
        return getMeasures(buckets);
    }
    return get(preferredMeasures, "items", []);
}

export function getBucketItemsWithExcludeByType(
    buckets: IBucket[],
    excludedBucket: string[],
    type: string[],
) {
    const includedBuckets = buckets.filter(
        (bucket: IBucket) => !includes(excludedBucket, bucket.localIdentifier),
    );
    return getAllItemsByType(includedBuckets, type);
}

export function getStackItems(buckets: IBucket[], itemTypes: string[] = [ATTRIBUTE]): IBucketItem[] {
    const preferredStacks = getPreferredBucket(buckets, [BucketNames.STACK, BucketNames.SEGMENT], itemTypes);

    return get(preferredStacks, "items", []);
}

export function getAttributeItems(buckets: IBucket[]): IBucketItem[] {
    return getAllAttributeItemsWithPreference(buckets, [
        BucketNames.LOCATION,
        BucketNames.VIEW,
        BucketNames.TREND,
    ]);
}

export function getAttributeItemsWithoutStacks(buckets: IBucket[]): IBucketItem[] {
    return getAttributeItems(buckets).filter(attribute => {
        return !includes(getStackItems(buckets), attribute);
    });
}

export function getAllCategoriesAttributeItems(buckets: IBucket[]): IBucketItem[] {
    const stackItemsWithDate = getStackItems(buckets, [ATTRIBUTE, DATE]);
    return getAttributeItems(buckets).filter((attribute: IBucketItem) => {
        return !includes(stackItemsWithDate, attribute);
    });
}

export function getAllAttributeItems(buckets: IBucket[]): IBucketItem[] {
    return getAllItemsByType(buckets, [ATTRIBUTE, DATE]);
}

function getAllMeasureItems(buckets: IBucket[]): IBucketItem[] {
    return getAllItemsByType(buckets, [METRIC]);
}

// get all attributes from buckets, but items from prefered buckets are first
export function getAllAttributeItemsWithPreference(buckets: IBucket[], preference: string[]): IBucketItem[] {
    const preferredAttributes = preference.reduce((acc, pref) => {
        const prefBucket = getPreferredBucket(buckets, [pref], [ATTRIBUTE, DATE]);
        return [...acc, ...get(prefBucket, "items", [])];
    }, []);
    const allBucketNames: string[] = buckets.map(bucket => get(bucket, "localIdentifier"));
    const otherBucketNames: string[] = allBucketNames.filter(bucketName => !includes(preference, bucketName));
    const allOtherAttributes = otherBucketNames.reduce(
        (attributes, bucketName) =>
            attributes.concat(getBucketItemsByType(buckets, bucketName, [ATTRIBUTE, DATE])),
        [],
    );
    return [...preferredAttributes, ...allOtherAttributes];
}

export function getDateItems(buckets: IBucket[]): IBucketItem[] {
    return getAttributeItemsWithoutStacks(buckets).filter(isDateBucketItem);
}

function hasItemsAboveLimit(bucket: IBucket, itemsLimit: number): boolean {
    const masterBucketItems = filterOutDerivedMeasures(bucket.items);
    return masterBucketItems.length > itemsLimit;
}

function applyItemsLimit(bucket: IBucket, itemsLimit: number): IBucket {
    if (itemsLimit !== undefined && hasItemsAboveLimit(bucket, itemsLimit)) {
        const newBucket = cloneDeep(bucket);

        newBucket.items = newBucket.items.slice(0, itemsLimit);
        return newBucket;
    }
    return bucket;
}

function applyUiConfigOnBucket(bucket: IBucket, bucketUiConfig: IBucketUiConfig): IBucket {
    return applyItemsLimit(bucket, get(bucketUiConfig, "itemsLimit"));
}

export function applyUiConfig(referencePoint: IExtendedReferencePoint): IExtendedReferencePoint {
    const buckets: IBucket[] = referencePoint.buckets;
    const uiConfig: IBucketsUiConfig = referencePoint.uiConfig.buckets;
    const newBuckets: IBucket[] = buckets.map((bucket: IBucket) =>
        applyUiConfigOnBucket(bucket, uiConfig[bucket.localIdentifier]),
    );
    set(referencePoint, "buckets", newBuckets);
    return referencePoint;
}

export function hasBucket(buckets: IBucket[], localIdentifier: string): boolean {
    return buckets.some(bucket => bucket.localIdentifier === localIdentifier);
}

export function findBucket(buckets: IBucket[], localIdentifier: string): IBucket {
    return buckets.find((bucket: IBucket) => get(bucket, "localIdentifier") === localIdentifier);
}

export function getBucketsByNames(buckets: IBucket[], names: string[]): IBucket[] {
    return buckets.filter((bucket: IBucket) => includes(names, get(bucket, "localIdentifier")));
}

export function getFirstMasterWithDerived(measureItems: IBucketItem[]): IBucketItem[] {
    const masters = filterOutDerivedMeasures(measureItems);
    const chosenMaster = masters[0];
    return measureItems.filter(
        measureItem =>
            measureItem.masterLocalIdentifier === chosenMaster.localIdentifier ||
            measureItem === chosenMaster,
    );
}

export function removeAllArithmeticMeasuresFromDerived(
    extendedReferencePoint: IExtendedReferencePoint,
): IExtendedReferencePoint {
    const originalBuckets = cloneDeep(extendedReferencePoint.buckets);
    forEach(extendedReferencePoint.buckets, bucket => {
        bucket.items = filterOutArithmeticMeasuresFromDerived(bucket.items, originalBuckets);
    });
    return extendedReferencePoint;
}

export function removeAllDerivedMeasures(
    extendedReferencePoint: IExtendedReferencePoint,
): IExtendedReferencePoint {
    forEach(extendedReferencePoint.buckets, bucket => {
        bucket.items = filterOutDerivedMeasures(bucket.items);
    });
    return extendedReferencePoint;
}

export function findMasterBucketItem(
    derivedBucketItem: IBucketItem,
    bucketItems: IBucketItem[],
): IBucketItem {
    return bucketItems.find(item => item.localIdentifier === derivedBucketItem.masterLocalIdentifier);
}

export function findMasterBucketItems(bucketItems: IBucketItem[]): IBucketItem[] {
    return bucketItems.filter(measure => !isDerivedBucketItem(measure));
}

export function findDerivedBucketItems(
    masterBucketItem: IBucketItem,
    bucketItems: IBucketItem[],
): IBucketItem[] {
    return bucketItems.filter(measure => measure.masterLocalIdentifier === masterBucketItem.localIdentifier);
}

export function findDerivedBucketItem(
    masterBucketItem: IBucketItem,
    bucketItems: IBucketItem[],
): IBucketItem {
    return bucketItems.find(
        bucketItem => bucketItem.masterLocalIdentifier === masterBucketItem.localIdentifier,
    );
}

export function hasDerivedBucketItems(masterBucketItem: IBucketItem, buckets: IBucket[]): boolean {
    return buckets.some(bucket =>
        bucket.items.some(
            bucketItem => bucketItem.masterLocalIdentifier === masterBucketItem.localIdentifier,
        ),
    );
}

export function getFilteredMeasuresForStackedCharts(buckets: IBucket[]) {
    const hasStacks = getStackItems(buckets).length > 0;
    if (hasStacks) {
        const limitedBuckets = limitNumberOfMeasuresInBuckets(buckets, 1);
        return getMeasureItems(limitedBuckets);
    }
    return getMeasureItems(buckets);
}

export function noRowsAndHasOneMeasure(buckets: VisualizationObject.IBucket[]): boolean {
    const measureBucket = buckets.find(bucket => bucket.localIdentifier === BucketNames.MEASURES);

    const rows = buckets.find(bucket => bucket.localIdentifier === BucketNames.VIEW);

    const hasOneMeasure = measureBucket && measureBucket.items.length === 1;
    const hasRows = rows && rows.items.length > 0;
    return Boolean(hasOneMeasure && !hasRows);
}

export function noColumnsAndHasOneMeasure(buckets: VisualizationObject.IBucket[]): boolean {
    const measureBucket = buckets.find(bucket => bucket.localIdentifier === BucketNames.MEASURES);

    const columns = buckets.find(bucket => bucket.localIdentifier === BucketNames.STACK);

    const hasOneMeasure = measureBucket && measureBucket.items.length === 1;
    const hasColumn = columns && columns.items.length > 0;
    return Boolean(hasOneMeasure && !hasColumn);
}

export function limitNumberOfMeasuresInBuckets(
    buckets: IBucket[],
    measuresLimitCount: number,
    tryToSelectDerivedWithMaster: boolean = false,
): IBucket[] {
    const allMeasures = getMeasureItems(buckets);

    let selectedMeasuresLocalIdentifiers: string[] = [];

    // try to select measures one per bucket
    buckets.forEach((bucket: IBucket) => {
        const currentBucketMeasures: IBucketItem[] = getAllBucketItemsByType(bucket, [METRIC]);

        if (currentBucketMeasures.length === 0) {
            return;
        }

        selectedMeasuresLocalIdentifiers = getLimitedMeasuresLocalIdentifiers(
            currentBucketMeasures,
            1,
            allMeasures,
            measuresLimitCount,
            tryToSelectDerivedWithMaster,
            selectedMeasuresLocalIdentifiers,
        );
    });

    // if it was not possible to select all measures one per bucket then limit them globally
    if (selectedMeasuresLocalIdentifiers.length < measuresLimitCount) {
        selectedMeasuresLocalIdentifiers = getLimitedMeasuresLocalIdentifiers(
            allMeasures,
            measuresLimitCount,
            allMeasures,
            measuresLimitCount,
            tryToSelectDerivedWithMaster,
            selectedMeasuresLocalIdentifiers,
        );
    }

    return pruneBucketMeasureItems(buckets, selectedMeasuresLocalIdentifiers);
}

function getLimitedMeasuresLocalIdentifiers(
    measures: IBucketItem[],
    measuresLimitCount: number,
    allMeasures: IBucketItem[],
    allMeasuresLimitCount: number,
    tryToSelectDerivedWithMaster: boolean,
    alreadySelectedMeasures: string[],
): string[] {
    let selectedMeasures: string[] = alreadySelectedMeasures;

    // try to select measures one by one together with their dependencies
    measures.forEach((measure: IBucketItem) => {
        if (selectedMeasures.length - alreadySelectedMeasures.length === measuresLimitCount) {
            return;
        }

        const measureDependencies = getDependenciesLocalIdentifiers(measure, allMeasures);
        const measureWithDependencies = [measure.localIdentifier, ...measureDependencies];

        if (tryToSelectDerivedWithMaster) {
            const derivedMeasures = getDerivedLocalIdentifiers(measure, allMeasures);
            const masterDerivedAndDependencies = [...measureWithDependencies, ...derivedMeasures];

            selectedMeasures = tryToSelectMeasures(
                masterDerivedAndDependencies,
                selectedMeasures,
                allMeasuresLimitCount,
            );
        }

        selectedMeasures = tryToSelectMeasures(
            measureWithDependencies,
            selectedMeasures,
            allMeasuresLimitCount,
        );
    });

    return selectedMeasures;
}

function getDerivedLocalIdentifiers(measure: IBucketItem, allMeasures: IBucketItem[]): string[] {
    const derivedMeasures = findDerivedBucketItems(measure, allMeasures);
    return derivedMeasures.map((derivedMeasure: IBucketItem) => derivedMeasure.localIdentifier);
}

function findMeasureByLocalIdentifier(
    localIdentifier: string,
    measures: IBucketItem[],
): IBucketItem | undefined {
    return measures.find((measure: IBucketItem) => measure.localIdentifier === localIdentifier);
}

function getDependenciesLocalIdentifiers(measure: IBucketItem, allMeasures: IBucketItem[]): string[] {
    const directDependencies: string[] = [];

    if (measure.masterLocalIdentifier) {
        directDependencies.push(measure.masterLocalIdentifier);
    }

    if (measure.operandLocalIdentifiers) {
        measure.operandLocalIdentifiers
            .filter(operandLocalIdentifier => operandLocalIdentifier !== null)
            .forEach((operandLocalIdentifier: string) => {
                const operandMeasure = findMeasureByLocalIdentifier(operandLocalIdentifier, allMeasures);
                if (operandMeasure !== undefined) {
                    directDependencies.push(operandLocalIdentifier);
                }
            });
    }

    const indirectDependencies: string[] = [];

    directDependencies.forEach((dependencyLocalIdentifier: string) => {
        const dependencyMeasure = findMeasureByLocalIdentifier(dependencyLocalIdentifier, allMeasures);
        const dependenciesOfDependency = getDependenciesLocalIdentifiers(dependencyMeasure, allMeasures);
        indirectDependencies.push(...dependenciesOfDependency);
    });

    return uniq([...directDependencies, ...indirectDependencies]);
}

function tryToSelectMeasures(measures: string[], alreadySelectedMeasures: string[], limit: number): string[] {
    const measuresToBePlaced = without(measures, ...alreadySelectedMeasures);

    if (measuresToBePlaced.length <= limit - alreadySelectedMeasures.length) {
        return [...alreadySelectedMeasures, ...measuresToBePlaced];
    }

    return alreadySelectedMeasures;
}

function pruneBucketMeasureItems(buckets: IBucket[], measureLocalIdentifiersToBeKept: string[]): IBucket[] {
    return buckets.map(
        (bucket: IBucket): IBucket => {
            const prunedItems = bucket.items.filter(
                (item: IBucketItem) =>
                    measureLocalIdentifiersToBeKept.indexOf(item.localIdentifier) > -1 ||
                    item.type !== METRIC,
            );

            return {
                ...bucket,
                items: prunedItems,
            };
        },
    );
}

export function isShowOnSecondaryAxis(item: IBucketItem): boolean {
    return get(item, SHOW_ON_SECONDARY_AXIS, false);
}

export function setMeasuresShowOnSecondaryAxis(items: IBucketItem[], value: boolean): IBucketItem[] {
    return items.map((item: IBucketItem) => ({
        ...item,
        [SHOW_ON_SECONDARY_AXIS]: value,
    }));
}

export function removeShowOnSecondaryAxis(items: IBucketItem[]): IBucketItem[] {
    return setMeasuresShowOnSecondaryAxis(items, null);
}

export function getAllMeasuresShowOnSecondaryAxis(buckets: IBucket[]): IBucketItem[] {
    return getAllItemsByType(buckets, [METRIC]).filter(isShowOnSecondaryAxis);
}

export function getItemsLocalIdentifiers(items: IBucketItem[]): string[] {
    return items.map((item: IBucketItem) => get(item, "localIdentifier", ""));
}

const getAvailableMeasureBucketsLocalIdentifiers = (type: VisType) =>
    (type === VisualizationTypes.BULLET && SUPPORTED_MEASURE_BUCKETS) || [];

export const getOccupiedMeasureBucketsLocalIdentifiers = (
    type: VisType,
    mdObject: VisualizationObject.IVisualizationObjectContent,
    executionResultData: Execution.DataValue[][],
): VisualizationObject.Identifier[] => {
    const availableMeasureBucketsLocalIdentifiers = getAvailableMeasureBucketsLocalIdentifiers(type);
    const buckets: VisualizationObject.IBucket[] = get(mdObject, "buckets", []);
    const notEmptyMeasureBucketsLocalIdentifiers = filterOutEmptyBuckets(buckets)
        .map(bucket => bucket.localIdentifier)
        .filter(
            (bucketLocalIdentifier: string) =>
                availableMeasureBucketsLocalIdentifiers.indexOf(bucketLocalIdentifier) >= 0,
        );
    return !isEmpty(notEmptyMeasureBucketsLocalIdentifiers)
        ? notEmptyMeasureBucketsLocalIdentifiers
        : availableMeasureBucketsLocalIdentifiers.slice(0, executionResultData.length);
};

export interface IMeasureBucketItemsLimit {
    localIdentifier: string;
    itemsLimit: number;
}

export const transformMeasureBuckets = (
    measureBucketItemsLimits: IMeasureBucketItemsLimit[],
    buckets: IBucket[],
) => {
    let unusedMeasures: IBucketItem[] = [];

    const newBuckets: IBucket[] = measureBucketItemsLimits.map(({ localIdentifier, itemsLimit }) => {
        const preferedBucketlocalIdentifiers: string[] =
            localIdentifier === BucketNames.MEASURES
                ? [BucketNames.MEASURES, BucketNames.SIZE]
                : localIdentifier === BucketNames.SECONDARY_MEASURES
                ? [BucketNames.SECONDARY_MEASURES, BucketNames.COLOR]
                : [localIdentifier];

        const preferredBucketItems = getPreferredBucketItems(buckets, preferedBucketlocalIdentifiers, [
            METRIC,
        ]);
        const measuresToBePlaced = preferredBucketItems.splice(0, itemsLimit);

        if (measuresToBePlaced.length === 0) {
            return {
                localIdentifier,
                items: unusedMeasures.splice(0, itemsLimit),
            };
        }

        unusedMeasures = [...unusedMeasures, ...preferredBucketItems];

        return {
            localIdentifier,
            items: measuresToBePlaced,
        };
    });

    return newBuckets.map((bucket: IBucket, bucketIndex: number) => {
        const bucketItemsLimit = measureBucketItemsLimits[bucketIndex].itemsLimit;

        const freeSlotsCount = bucketItemsLimit - bucket.items.length;
        if (freeSlotsCount === 0) {
            return bucket;
        }

        return {
            ...bucket,
            items: [...bucket.items, ...unusedMeasures.splice(0, freeSlotsCount)],
        };
    });
};
