import { isArrayOfAny, emptyObject, isObject, isString } from "./type-utils";
import { deepClone, getObjectKeys } from "./datum-utils";
import { MergeCode, UpdateCode, mergeScalarField, mergeVectorField } from "./merge-low";
import { deepDiffTyped } from "./diff-high";
import { PatchResult, deepPatchLog } from "./patch-low";

export type MergePerms = {
    enable: boolean;
    insert?: boolean;
    update?: boolean;
    unset?: boolean;
};

export function updateCodeInfo(
    mergeCode: MergeCode,
): MergePerms {
    if (!isString(mergeCode)) {
        return { enable: false };
    }
    const allowUnset = ([
        UpdateCode.Y, UpdateCode.D, UpdateCode.U,
        UpdateCode.XR, UpdateCode.XD, UpdateCode.XI] as string[]
    ).includes(mergeCode);
    const allowInsert = ([
        UpdateCode.Y, UpdateCode.I, UpdateCode.B,
        UpdateCode.XR, UpdateCode.XM, UpdateCode.XS, UpdateCode.XF] as string[]
    ).includes(mergeCode);
    const allowUpdate = ([
        UpdateCode.Y, UpdateCode.H, UpdateCode.U, UpdateCode.B,
        UpdateCode.XR, UpdateCode.XS, UpdateCode.XF,
        UpdateCode.XM, UpdateCode.XI, UpdateCode.XD] as string[]
    ).includes(mergeCode);
    return {
        insert: allowInsert,
        update: allowUpdate,
        unset: allowUnset,
        enable: allowInsert || allowUpdate || allowUnset,
    };
}

//-----------------------------------------------------------------------------

/**
 * merges every top-level key present in source
 * C, T not supported here
 * @returns if target was changed
 */
export function shallowMerge(
    target: any,
    source: any,
    scalarCode: MergeCode,
    vectorCode?: MergeCode,
    excludeKeys?: string[],
    includeKeys?: string[], //for delete
): boolean {
    const sourceKeys = getObjectKeys(source, excludeKeys, includeKeys);
    if (!sourceKeys?.length) {
        return false;
    }
    let changed = false;
    for (const label of sourceKeys) {
        if (isArrayOfAny(target[label]) || isArrayOfAny(source[label])) {
            //fails if target is not array
            if (mergeVectorField(target, source, label, vectorCode ?? scalarCode)) {
                changed = true;
            }
            continue;
        }
        if (mergeScalarField(target, source, label, scalarCode)) {
            changed = true;
        }
    }
    return changed;
}

/**
 * shallow merge but into a clone of target
 * @returns new object with merged result
 */
export function immutableMerge(
    target: any,
    source: any,
    scalarCode: MergeCode,
    vectorCode?: MergeCode,
): any {
    const targetCopy = deepClone(target);
    shallowMerge(targetCopy, source, scalarCode, vectorCode);
    return targetCopy;
};

//-----------------------------------------------------------------------------

/**
 * recursively merges any unstructured datum
 * nesting can be disabled or treated as scalar
 * C, T not supported here
 * @returns if target was changed
 */
export function deepMerge(
    target: { [key: string]: any },
    source: { [key: string]: any },
    scalarCode: MergeCode,
    vectorCode: MergeCode,
    nestedCode: MergeCode,
): boolean {
    const sourceKeys = getObjectKeys(source);
    if (!sourceKeys?.length) {
        return false;
    }
    let changed = false;
    for (const label of sourceKeys) {
        if (isArrayOfAny(target[label]) || isArrayOfAny(source[label])) {
            changed = mergeVectorField(target, source, label, vectorCode) || changed;
            continue;
        }
        //recursive call for objects
        //todo should nest for empty target
        if (isObject(target[label]) && isObject(source[label])) {
            if (nestedCode === UpdateCode.N)
                continue;
            if (nestedCode === UpdateCode.Y) {
                changed = mergeScalarField(target, source, label, scalarCode) || changed;
                continue;
            }
            changed = deepMerge(target[label], source[label],
                nestedCode.startsWith("X") ? scalarCode : nestedCode,
                nestedCode.startsWith("X") ? nestedCode : vectorCode,
                nestedCode) || changed;
            continue;
        }
        changed = mergeScalarField(target, source, label, scalarCode) || changed;
    }
    return changed;
}

/**
 * deep merge but into a clone of target
 * @returns new object with merged result
 */
export function immutableDeepMerge(
    target: { [key: string]: any },
    source: { [key: string]: any },
    scalarCode: MergeCode,
    vectorCode?: MergeCode,
    nestedCode?: MergeCode,
): any {
    const targetCopy = deepClone(target);
    deepMerge(targetCopy, source,
        scalarCode,
        vectorCode ?? scalarCode,
        nestedCode ?? scalarCode,
    );
    return targetCopy;
};

/**
 * simulate diff generated by deep merge
 * target object is not modified
 * deletes are lost in the delta object
 * @returns diff or false if no changes
 */
export function diffFromMerge(
    target: { [key: string]: any },
    source: { [key: string]: any },
    scalarCode: MergeCode,
    vectorCode?: MergeCode,
    nestedCode?: MergeCode,
    orderInd: boolean = false,
): any | false {
    const targetCopy = immutableDeepMerge(target, source, scalarCode, vectorCode, nestedCode);
    const delta = deepDiffTyped(target, targetCopy, orderInd);
    return emptyObject(delta) ? false : delta;
}

/**
 * simulate patch generated by deep merge
 * target object is not modified
 * @returns patch array or false if no changes
 */
export function patchFromMerge(
    target: { [key: string]: any },
    source: { [key: string]: any },
    scalarCode: MergeCode,
    vectorCode?: MergeCode,
    nestedCode?: MergeCode,
    orderInd: boolean = false,
): PatchResult[] | false {
    const targetCopy = immutableDeepMerge(target, source, scalarCode, vectorCode, nestedCode);
    const patch = deepPatchLog(target, targetCopy, orderInd, true);
    return !patch?.length ? false : patch;
}
