import { create as diffCreate } from "jsondiffpatch";
import { type numberFormatOptions, REGEX_ISO8601_LOOSE } from './Atomic.ts';
import { diff, applyChangeset, type Changeset, type Options } from 'json-diff-ts';
// may want to return to this one day
// but currently the jsondiffpatch formatter is the best console/ansi diff output for humans :(
//import {DiffOptions, DiffOptionsColor, diff as jestDiff} from 'jest-diff';
import clone from "clone";
import ConsoleFormatter from "jsondiffpatch/formatters/console";
import assert from "node:assert";
import dayjs from "dayjs";
import { Traverse } from "neotraverse/modern";
import { serializeError } from "serialize-error";

const console = new ConsoleFormatter();

export const jdiff = diffCreate({
    propertyFilter(name, context) {
        return name !== 'lifecycle';
    },
    cloneDiffValues: true
    //omitRemovedValues: true
});

const diffOptions: Options = {
    /*arrayIdentityKeys: {artists: '$value'},*/
    keysToSkip: ['playDate','playDateCompleted','listenRanges']
};

export const diffObjects = (a: object, b: object) => {
    return diff(a, b, diffOptions);
}

export const patchObject = <T>(a: T, b: Changeset): T => {
    return applyChangeset(clone(a), b);
}

// const jestDiffOptions: DiffOptions = {
//     aAnnotation: 'Old', 
//     bAnnotation: 'New', 
//     aColor: chalk.red,
//     bColor: chalk.green
// }

export const diffObjectsConsoleOutput = (a: object, b: object, showUnchanged: boolean = false) => {
    //return jestDiff(a, b, jestDiffOptions);

    const left = JSON.parse(JSON.stringify(a));
    return console.format(jdiff.diff(left, JSON.parse(JSON.stringify(b))), showUnchanged ? left : undefined);
}

export const formatNumber = (val: number | string, options?: numberFormatOptions) => {
    const {
        toFixed = 2, defaultVal = null, prefix = '', suffix = '', round,
    } = options || {};
    let parsedVal = typeof val === 'number' ? val : Number.parseFloat(val);
    if (Number.isNaN(parsedVal)) {
        return defaultVal;
    }
    if (!Number.isFinite(val)) {
        return 'Infinite';
    }
    let prefixStr = prefix;
    const { enable = false, indicate = true, type = 'round' } = round || {};
    if (enable && !Number.isInteger(parsedVal)) {
        switch (type) {
            case 'round':
                parsedVal = Math.round(parsedVal);
                break;
            case 'ceil':
                parsedVal = Math.ceil(parsedVal);
                break;
            case 'floor':
                parsedVal = Math.floor(parsedVal);
        }
        if (indicate) {
            prefixStr = `~${prefix}`;
        }
    }
    const localeString = parsedVal.toLocaleString(undefined, {
        minimumFractionDigits: toFixed,
        maximumFractionDigits: toFixed,
    });
    return `${prefixStr}${localeString}${suffix}`;
};

export const generateArray = <T = any>(size: number, gen: (index: number) => T): T[] => {
    return Array.from(Array(size), (v,k) => gen(k));
}

/** Return an array in chunks
 * 
 * https://stackoverflow.com/a/8495740/1469797
 */
export const chunkArray = <T>(chunkSize: number, arr: T[]): T[][] => {
    assert(chunkSize !== 0, 'chunkSize cannot be 0');
    const chunks: T[][] = [];
    for (let i = 0; i < arr.length; i += chunkSize) {
        const chunk = arr.slice(i, i + chunkSize);
        chunks.push(chunk);
    }
    return chunks;
}

export const asDayjsHydratedObject = <T, U>(obj: T): U => {
  const cloned = clone(obj);
  new Traverse(cloned).forEach((ctx, x) => {

     if (typeof x === 'string' && REGEX_ISO8601_LOOSE.test(x)) {
        ctx.update(dayjs(x), true);
    }
  });
  return cloned as unknown as U;
};

export const asErrorSerializedObject = <T, U>(obj: T): U => {
  const cloned = clone(obj);
  new Traverse(cloned).forEach((ctx, x) => {

     if (x !== null && typeof x === 'object' && x instanceof Error) {
        ctx.update(serializeError(x), true);
    }
  });
  return cloned as unknown as U;
};

/**
 * Get indexes of all elements in array that make the function return true
 * 
 * @see https://stackoverflow.com/a/20798567/1469797
 */
export const getAllIndexes = <T>(arr: T[], truthyFunc: (val: T) => boolean) => {
    const indexes = [];
    for(let i = 0; i < arr.length; i++)
        if (truthyFunc(arr[i]))
            indexes.push(i);
    return indexes;
};
export const removeUndefinedKeys = <T extends Record<string, any>>(obj: T, returnUndefined: boolean = true): T | undefined => {
    const newObj: any = {};
    Object.keys(obj).forEach((key) => {
        if (Array.isArray(obj[key])) {
            newObj[key] = obj[key];
        } else if (obj[key] === Object(obj[key])) {
            // dumb assign nested objects
            // bc they may be third party library-objects that use prototyping and we don't want to mess with
            newObj[key] = obj[key];
        } else if (obj[key] !== undefined) {
            newObj[key] = obj[key];
        }
    });
    if (Object.keys(newObj).length === 0) {
        if (returnUndefined) {
            return undefined;
        }
        return newObj;
    }
    Object.keys(newObj).forEach(key => {
        if (newObj[key] === undefined || (null !== newObj[key] && typeof newObj[key] === 'object' && Object.keys(newObj[key]).length === 0)) {
            delete newObj[key];
        }
    });
    //Object.keys(newObj).forEach(key => newObj[key] === undefined || newObj[key] && delete newObj[key])
    return newObj;
};
