// (C) 2007-2020 GoodData Corporation
import { VisualizationObject, IVisualizationWidget } from "@gooddata/typings";
import isEmpty from "lodash/isEmpty";
import omit from "lodash/omit";
import isArray from "lodash/isArray";
import isObject from "lodash/isObject";
import isString from "lodash/isString";
import stringify from "json-stable-stringify";
import { v4 as uuid } from "uuid";

import { IProperties } from "./interfaces";
import { isUri } from "./DataLayer/helpers/uri";
/*
 * Helpers
 */
const getReferenceValue = (id: string, references: VisualizationObject.IReferenceItems) => references[id];
const getReferenceId = (value: string, references: VisualizationObject.IReferenceItems) =>
    Object.keys(references).find(id => references[id] === value);

type IdGenerator = () => string;

const defaultIdGenerator: IdGenerator = () => uuid().replace(/-/g, "");

type StringTransformation = (value: string) => string;

/**
 * Recursively traverses the object and tries to apply a conversion to every string value
 */
const traverse = (obj: any, convert: StringTransformation): any => {
    if (isArray(obj)) {
        return obj.map(a => traverse(a, convert));
    } else if (isObject(obj)) {
        const object = obj as Record<string, any>;
        return Object.keys(object).reduce((result: any, key: string) => {
            result[key] = traverse(object[key], convert);
            return result;
        }, {} as any);
    } else if (isString(obj)) {
        return convert(obj);
    } else {
        return obj;
    }
};

interface IConversionResult {
    convertedProperties: IProperties;
    convertedReferences: VisualizationObject.IReferenceItems;
}

type ConversionFunction = (
    originalProperties: IProperties,
    originalReferences: VisualizationObject.IReferenceItems,
    idGenerator: IdGenerator,
) => IConversionResult;

export type IObjectWithProperties = VisualizationObject.IVisualizationObject | IVisualizationWidget;

export type ReferenceConverter = (
    mdObject: IObjectWithProperties,
    idGenerator?: IdGenerator,
) => IObjectWithProperties;

const createConverter = (conversionFunction: ConversionFunction): ReferenceConverter => <
    T extends IObjectWithProperties
>(
    mdObject: T,
    idGenerator: IdGenerator = defaultIdGenerator,
): T => {
    const { content } = mdObject;
    if (!content) {
        return mdObject;
    }

    const { properties } = content;
    if (!properties) {
        return mdObject;
    }

    // prepare result objects
    const originalProperties: IProperties = JSON.parse(properties);
    const originalReferences = content.references || {};

    const { convertedProperties, convertedReferences } = conversionFunction(
        originalProperties,
        originalReferences,
        idGenerator,
    );

    // set the new properties and references
    const referencesProp = isEmpty(convertedReferences) ? undefined : { references: convertedReferences };
    const result: T = {
        ...mdObject,
        content: {
            ...omit(mdObject.content, "references"),
            properties: stringify(convertedProperties),
            ...referencesProp,
        },
    };

    return result;
};

/*
 * Conversion from References to URIs
 */
const convertReferenceToUri = (
    references: VisualizationObject.IReferenceItems,
): StringTransformation => value => getReferenceValue(value, references) || value;

/**
 * Converts reference based values to actual URIs
 *
 * @param mdObject The object to convert properties of
 * @param [idGenerator=uuid] Function that returns unique ids
 */
export const convertReferencesToUris = createConverter((originalProperties, originalReferences) => {
    const convertedProperties = traverse(originalProperties, convertReferenceToUri(originalReferences));

    return {
        convertedProperties,
        convertedReferences: originalReferences,
    };
});

/*
 * Conversion from URIs to References
 */
const createUriToReferenceConverter = (
    originalReferences: VisualizationObject.IReferenceItems,
    idGenerator: IdGenerator,
) => {
    const convertedReferences: VisualizationObject.IReferenceItems = {};

    return {
        convertedReferences,
        conversion: (value: string) => {
            if (!isUri(value)) {
                return value;
            }

            const id =
                getReferenceId(value, originalReferences) || // try to reuse original references
                getReferenceId(value, convertedReferences) || // or use already converted new references
                idGenerator(); // or get a completely new id

            convertedReferences[id] = value;
            return id;
        },
    };
};

/**
 * Converts URIs to reference based values
 *
 * @param mdObject The object to convert properties of
 * @param [idGenerator=uuid] Function that returns unique ids
 */
export const convertUrisToReferences = createConverter(
    (originalProperties, originalReferences, idGenerator) => {
        const converter = createUriToReferenceConverter(originalReferences, idGenerator);
        const convertedProperties = traverse(originalProperties, converter.conversion);

        return {
            convertedProperties,
            convertedReferences: converter.convertedReferences,
        };
    },
);
