import dayjs, { Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration.js";
import isBetween from "dayjs/plugin/isBetween.js";
import relativeTime from "dayjs/plugin/relativeTime.js";
import timezone from "dayjs/plugin/timezone.js";
import utc from "dayjs/plugin/utc.js";
import {
    AmbPlayObject,
    ArtistCredit,
    PlayData,
    PlayObject,
    SCROBBLE_TS_SOC_END,
    SCROBBLE_TS_SOC_START,
    ScrobbleTsSOC,
    TrackStringOptions
} from "./Atomic.js";
import { DELIMETERS_REGEX, DELIMITERS } from "../backend/common/infrastructure/Atomic.js";
import { parseRegexSingle } from "@foxxmd/regex-buddy-core";

dayjs.extend(utc)
dayjs.extend(isBetween);
dayjs.extend(relativeTime);
dayjs.extend(duration);
dayjs.extend(timezone);

export const longestString = (strings: any) => strings.reduce((acc: any, curr: any) => curr.length > acc ? curr.length : acc, 0);
export const truncateStringArrToLength = (length: any, truncStr = '...') => {
    const truncater = truncateStringToLength(length, truncStr);
    return (strings: any) => strings.map(truncater);
}
export const truncateStringToLength = (length: any, truncStr = '...') => (val: any = '') => {
    if (val === null) {
        return '';
    }
    const str = typeof val !== 'string' ? val.toString() : val;
    return str.length > length ? `${str.slice(0, length)}${truncStr}` : str;
}
export const defaultTrackTransformer = (input: any, data: AmbPlayObject, hasExistingParts: boolean = false) => hasExistingParts ? `- ${input}` : input;
export const defaultReducer = (acc, curr) => `${acc} ${curr}`;
export const defaultArtistFunc = (a: string[]) => a === undefined ? '' : a.join(' / ');
export const defaultAlbumFunc = (input: any, data: AmbPlayObject, hasExistingParts: boolean = false) => {
    if(input === undefined) {
        return undefined;
    }
    return hasExistingParts ? `--- ${input}` : input
};
export const defaultTimeFunc = (t: Dayjs | undefined, i?: ScrobbleTsSOC) => t === undefined ? '@ N/A' : `@ ${t.local().format()} ${i === undefined ? '' : (i === SCROBBLE_TS_SOC_START ? '(S)' : '(C)')}`;
export const defaultTimeFromNowFunc = (t: Dayjs | undefined) => t === undefined ? undefined : `(${t.local().fromNow()})`;
export const defaultCommentFunc = (c: string | undefined) => c === undefined ? undefined : `(${c})`;
// TODO replace with genGroupIdStr and refactor Platform types/etc. into core Atomic
export const defaultPlatformFunc = (d: string | undefined, u: string | undefined, s: string | undefined) => combinePartsToString([d ?? 'NoDevice', u ?? 'SingleUser',s !== undefined ? `Session${s}` : undefined]);
export const defaultBuildTrackStringTransformers = {
    artists: defaultArtistFunc,
    track: defaultTrackTransformer,
    album: defaultAlbumFunc,
    time: defaultTimeFunc,
    timeFromNow: defaultTimeFromNowFunc,
    comment: defaultCommentFunc,
    platform: defaultPlatformFunc
}
export const buildTrackString = <T = string>(playObj: AmbPlayObject, options: TrackStringOptions<T> = {}): T => {
    const {
        include = ['time', 'artist', 'track'],
        transformers: {
            artists: artistsFunc = defaultBuildTrackStringTransformers.artists,
            album: albumFunc = defaultBuildTrackStringTransformers.album,
            track: trackFunc = defaultBuildTrackStringTransformers.track,
            time: timeFunc = defaultBuildTrackStringTransformers.time,
            timeFromNow = defaultBuildTrackStringTransformers.timeFromNow,
            comment: commentFunc = defaultBuildTrackStringTransformers.comment,
            platform: platformFunc = defaultBuildTrackStringTransformers.platform,
            reducer = arr => arr.join(' ') // (acc, curr) => `${acc} ${curr}`
        } = {},
    } = options;
    const {
        data: {
            artists,
            album,
            track,
            playDate,
            playDateCompleted
        } = {},
        meta: {
            trackId,
            scrobbleTsSOC = SCROBBLE_TS_SOC_START,
            comment,
            deviceId,
            user,
            sessionId
        } = {},
    } = playObj;

    let pd: Dayjs;
    let usedTsSOC: ScrobbleTsSOC = scrobbleTsSOC;
    if(scrobbleTsSOC === SCROBBLE_TS_SOC_END && playDateCompleted !== undefined) {
        pd = typeof playDateCompleted === 'string' ? dayjs(playDateCompleted) : playDateCompleted;
    } else {
        usedTsSOC = SCROBBLE_TS_SOC_START;
        pd = typeof playDate === 'string' ? dayjs(playDate) : playDate;
    }

    const strParts: (T | string)[] = [];
    if(include.includes('platform')) {
        strParts.push(platformFunc(deviceId, user, include.includes('session') ? sessionId : undefined))
    } else if(include.includes('session') && sessionId !== undefined) {
        strParts.push(`(Session ${sessionId})`);
    }
    if (include.includes('trackId') && trackId !== undefined) {
        strParts.push(`(${trackId})`);
    }
    if (include.includes('artist')) {
        strParts.push(artistsFunc(artistCreditsToNames(artists)))
    }
    if (include.includes('track')) {
        strParts.push(trackFunc(track, playObj, strParts.length > 0));
    }
    if (include.includes('album')) {
        strParts.push(albumFunc(album, playObj, strParts.length > 0));
    }
    if (include.includes('time')) {
        strParts.push(timeFunc(pd, usedTsSOC));
    }
    if (include.includes('timeFromNow')) {
        const tfn = timeFromNow(pd);
        if (tfn !== undefined) {
            strParts.push(tfn)
        }

    }
    if (include.includes('comment')) {
        const cfn = commentFunc(comment);
        if(cfn !== undefined) {
            strParts.push(cfn);
        }
    }
    // @ts-ignore
    return reducer(strParts); //strParts.join(' ');
}

export const buildPlayHumanDiffable = (play: PlayData, options?: {expandMeta?: boolean}): string => {
    const {
        expandMeta = false
    } = options || {};

    let meta: string[] = [];
    if(play.meta !== undefined) {
        for(const [metaType,metaObject] of Object.entries(play.meta)) {
            for(const [metaKey, metaValue] of Object.entries(metaObject)) {
                if(metaValue === undefined) {
                    continue;
                }
                const id  = `${metaType}-${metaKey}`;
                if(expandMeta) {
                    meta.push(`${id}: ${metaValue}`);
                } else {
                    meta.push(id);
                }
            }
        }
    }
    let metaStr = '(None)';
    if(meta.length > 0) {
        if(expandMeta) {
            metaStr = `\n${meta.map(x => `* ${x}`).join('\n')}`;
        } else {
            metaStr = meta.join(', ');
        }
    }
    const parts: string[] = [
        `${'Title'.padEnd(13)}: ${play.track ?? '(None)'}`,
        `${'Artists'.padEnd(13)}: ${play.artists === undefined || play.artists.length === 0 ? '(None)' : play.artists.join(', ')}`,
        `${'Album Artists'.padEnd(13)}: ${play.albumArtists === undefined || play.albumArtists.length === 0 ? '(None)' : play.albumArtists.join(', ')}`,
        `${'Album'.padEnd(13)}: ${play.album ?? '(None)'}`,
        `${'Meta'.padEnd(13)}: ${metaStr}`
    ];

    const final = parts.join('\n');
    return final;
} 

export const slice = (str: string, index: number, count: number, add?: string): string => {
    // We cannot pass negative indexes directly to the 2nd slicing operation.
    if (index < 0) {
        index = str.length + index;
        if (index < 0) {
            index = 0;
        }
    }

    return str.slice(0, index) + (add || "") + str.slice(index + count);
}
export const capitalize = (str: any) => {
    return str.charAt(0).toUpperCase() + str.slice(1)
}

/**
 * Split a string-ish variable by a list of deliminators and return the first actually split array or default to returning the string as the first element.
 *
 * Returns empty array, or user defined value, if variable is undefined / null / not a string / or an empty string.
 * */
export const splitByFirstFound = <T>(str: any, delims = [','], onNotAStringVal: T): string[] | T => {
    if(str === undefined || str === null || typeof str !== 'string' || str.trim() === '') {
        return onNotAStringVal;
    }
    for(const d of delims) {
        const split = str.split(d);
        if(split.length > 1) {
            return split;
        }
    }
    return [str];
}

/**
 * Split a string-ish variable by a regex and return the first actually split array or default to returning the string as the first element.
 *
 * Returns empty array, or user defined value, if variable is undefined / null / not a string / or an empty string.
 * */
export const splitByFirstRegexFound = <T>(str: any, onNotAStringVal: T, delimsReg: RegExp = DELIMETERS_REGEX): string[] | T => {
    if (str === undefined || str === null || typeof str !== 'string' || str.trim() === '') {
        return onNotAStringVal;
    }
    const res = parseRegexSingle(delimsReg, str);
    if (res !== undefined) {
        return [str.slice(0, res.index - 1), str.slice(res.index + 1)];
    }
    return [str];
}

/**
 * Returns value if it is a non-empty string or returns default value
 * */
export const nonEmptyStringOrDefault = <T = undefined>(str: any, defaultVal: T = undefined): string | T => {
    if (str === undefined || str === null || typeof str !== 'string' || str.trim() === '') {
        return defaultVal;
    }
    return str;
}
export const combinePartsToString = (parts: any[], glue: string = '-'): string | undefined => {
    const cleanParts: string[] = [];
    for (const part of parts) {
        if (part === null || part === undefined) {
            continue;
        }
        if (Array.isArray(part)) {
            const nestedParts = combinePartsToString(part, glue);
            if (nestedParts !== undefined) {
                cleanParts.push(nestedParts);
            }
        } else if (typeof part === 'object') {
            // hope this works
            cleanParts.push(JSON.stringify(part));
        } else if (typeof part === 'string') {
            if (part.trim() !== '') {
                cleanParts.push(part);
            }
        } else {
            cleanParts.push(part.toString());
        }
    }
    if (cleanParts.length > 0) {
        return cleanParts.join(glue);
    }
    return undefined;
}

export const arrayListOxfordAnd = (list: string[], joiner: string, finalJoiner: string, spaced: boolean = true): string => {
    if(list.length === 1) {
        return list[0];
    }
    const start = list.slice(0, list.length - 1);
    const end = list.slice(list.length - 1);

    const joinerProper = joiner === ',' ? ', ' : (spaced ? ` ${joiner} ` : joiner);
    const finalProper = spaced ? ` ${finalJoiner} ` : finalJoiner;

    return [start.join(joinerProper), end].join(joiner === ',' && spaced ? `,${finalProper}` : finalProper);
}

export const arrayListAnd = (list: string[], joiner: string, finalJoiner: string, spaced: boolean = true): string => {
    if(list.length === 1) {
        return list[0];
    }
    const start = list.slice(0, list.length - 1);
    const end = list.slice(list.length - 1);

    const joinerProper = joiner === ',' ? ', ' : (spaced ? ` ${joiner} ` : joiner);
    const finalProper = spaced ? ` ${finalJoiner} ` : finalJoiner;

    return [start.join(joinerProper), end].join(finalProper);
}

export const safeStringify = (json: unknown) => JSON.stringify(json, null, 2);
export const findDelimiters = (str: string, delimiters = DELIMITERS) => {
    const found: string[] = [];
    for (const d of delimiters) {
        if (str.indexOf(d) !== -1) {
            found.push(d);
        }
    }
    if (found.length === 0) {
        return undefined;
    }
    return found;
};

export const containsDelimiters = (str: string) => null !== str.match(/[,&/\\]+/i);

const NUMBERS_REGEX = new RegExp(/^\s*\d+\s*$/);
export const stringIsOnlyNumbers = (str: string) => NUMBERS_REGEX.test(str);

export const artistNamesToCredits = (names: string[] | undefined): ArtistCredit[] => names === undefined ? undefined : names.map((x) => ({name: x}));
export const artistNameToCredit = (name: string | undefined): ArtistCredit => name === undefined ? undefined : ({ name });
export const artistCreditToName = (a: ArtistCredit): string => a.name;
export const artistCreditsToNames = (a: ArtistCredit[]): string[] => a.map((x) => x.name);