import dayjs, {Dayjs} from "dayjs";
import 'dayjs/locale/zh-cn'
import updateLocale from 'dayjs/plugin/updateLocale'
import request from "~/utils/request";
import {Toast, showFailToast} from "vant";
import {hwUpYun, UpyunAccess} from '~/utils/upyun';
import pinyin from "~/utils/pinyin";
import {
    AiTaskStatuses,
    Comment, CommentContent,
    CommonTask,
    RefResource, ResourceNote,
    TaskStatuses,
    TaskTypes,
    UnauthorizedError,
    UploadedFile,
    UploadPhoto
} from "~/types";
import useUser from "~/store/user";

dayjs.extend(updateLocale)
dayjs.locale('zh-cn')
dayjs.updateLocale('zh-cn', {weekStart: 0})

enum DateFormatter {
    Date = 'YYYY-MM-DD',
    DateTimeSave = 'YYYY-MM-DDTHH:mm:ss',
    DateTime = 'YYYY-MM-DD HH:mm:ss'
}

const formatDate = (date: Dayjs | Date | undefined | null | string, format: string = DateFormatter.Date) => {

    if (!date) return "";

    return dayjs(date).format(format)
}

const getWeekEnd = (start: Dayjs | Date | string, format: string = DateFormatter.Date) => {
    if (!start) return "";
    return dayjs(start).add(6, 'day').format(format)
}
const isIOS = () => {
    let ua = navigator.userAgent;
    return ua.includes('iPhone') || ua.includes('iPad');
}

const isWxEnv = () => navigator.userAgent.includes('miniProgram');

const BaseUrl = '/';
const HtyUnionIDToken = 'unionid';
const HtyAuthToken: string = 'Authorization';
const HtySudoToken: string = 'HtySudoerToken';
const WebViewEntryUrl: string = 'WebViewEntryUrl';
const HtyHostHeader: string = 'HtyHost';
const CurrentUserRole: string = 'CurrentUserRole';
const CurrentOrgId: string = 'CurrentOrgId';
const UserStatusKey: string = 'UserStatus';
const CurrentUserName: string = 'CurrentUserName';
const CurrentTeacherId: string = 'CurrentTeacherId';
const PiyueLockMinutes: number = 30

const list2indexed = (list: any[]) => {

    let indices = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('');
    let indexed = list.map(item => {
        let temp = {...item};
        let char = item.real_name.charAt(0);
        if (/[a-z]/i.test(char)) {
            temp.index = char.toUpperCase();
        } else {
            // @ts-ignore
            temp.index = (pinyin[char] || '#').toUpperCase();
        }
        return temp;
    })
    return indices.map(index => ({
        index, records: indexed.filter(x => x.index === index)
    }))
}

const uuid = (): string => {
    let s = [], hexDigits = "0123456789abcdef";
    for (let i = 0; i < 36; i++) {
        s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
    }
    s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
    s[19] = hexDigits.substr((Number(s[19]) & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    s[8] = s[13] = s[18] = s[23] = "-";

    return s.join("");
}

const ImageHelper = {
    upload_by_form: async (file: any): Promise<UploadedFile | undefined> => {
        const { r, d, e } = await hwUpYun.upload(file.file, 'music-room');
        if (r) {
            let id = await ImageHelper.create_resource(d, file.file.name);
            if (id) {
                return { url: d, hty_resource_id: id };
            }
        } else {
            showFailToast(e);
        }
        // if sudo token failed, retry
        if (d === UnauthorizedError.HtySudoerTokenErr) {
            return useUser().sudo2().then(() => ImageHelper.upload_by_form(file))
        }
        showFailToast(e);
    },
    combine_by_form: async (files: any): Promise<string | undefined> => {
        let data = new FormData();
        if (!files.length) files = [files];
        files.forEach((f: any) => data.append('files[]', f.file))
        let {r, d, e} = await request({
            url: '/api/ngx/image/form_upload_to_combine', method: 'POST', data,
            // @ts-ignore
            headers: {'Content-Type': 'multipart/form-data'},
        })
        if (r) {
            return d;
        }
        showFailToast(e);
    },
    compress_by_form: async (file: File): Promise<string | undefined> => {
        let data = new FormData();
        data.append('file', file)
        let {r, d, e, statusCode} = await request({
            url: '/api/ngx/image/form_upload_to_compress', method: 'POST', data,
            // @ts-ignore
            headers: {'Content-Type': 'multipart/form-data'},
        })
        const dSnippet =
            typeof d === 'string' ? d.trim().slice(0, 160) : JSON.stringify(d)?.slice(0, 160);
        console.info('[huike][compress_by_form]', {
            ok: r,
            statusCode,
            err: e,
            bodySnippet: dSnippet,
            fileName: file?.name,
            fileSize: file?.size,
        })
        if (r) {
            const raw = typeof d === 'string' ? d.trim() : '';
            if (raw && /^https?:\/\//i.test(raw) && !/\s/.test(raw)) {
                return raw;
            }
            showFailToast('图片上传返回异常，请重试');
            return undefined;
        }
        showFailToast(e);
    },
    upload_by_wx: async (media_id: string): Promise<UploadedFile | undefined> => {
        let { r, d, e } = await request({
            url: '/api/ngx/image/wx_upload_single',
            method: 'POST', data: {media_id}
        });
        if (r) {
            let id = await ImageHelper.create_resource(d);
            if (id) {
                return { hty_resource_id: id, url: d };
            }
        }
        showFailToast(e)
    },
    combine_by_wx: async (media_ids: string[]): Promise<string | undefined> => {
        console.log('combine by wx...', media_ids)
        let { r, d, e } = await request({
            url: '/api/ngx/image/combine',
            method: 'POST', data: {payload: {images: media_ids, image_origin: "WX"}}
        });
        if (r) {
            return d;
        }
        showFailToast(e)
    },
    load: async (src: string) => {
        return new Promise<HTMLImageElement>((resolve, reject) => {
            let image = new Image();
            image.onload = () => resolve(image);
            image.onerror = reject
            image.src = src;
        })
    },
    create_resource: async (
        url: string,
        filename?: string,
        extra?: { res_type?: string },
    ) => {
        const {r, d, e} = await request({
            url: '/api/v1/uc/create_hty_resource', method: 'POST',
            data: { filename, url, ...extra }
          });
        if(r) {
            return d
        }
        console.error('create resource failed...', e)
    }
}

const pad = (n: number, c: number = 2): string => {
    let s = n.toString();
    while (s.length < c) {
        s = "0" + s;
    }
    return s;
}

const is_equal = (source: any, target: any) => {

    if (typeof source != typeof target) return false;

    if (typeof source === "object" && source && target) {
        if (source instanceof Array) {
            if (source.length !== target.length) return false;
            for(let i = 0; i < source.length; i++) {
                if (!is_equal(source[i], target[i])) {
                    return false;
                }
            }
            return true;
        }

        let sKeys = Object.keys(source), tKeys = Object.keys(target);
        if (sKeys.length !== tKeys.length) return false;
        for(let i = 0; i < sKeys.length; i++) {
            let key = sKeys[i];
            if (tKeys.includes(key)) {
                if (!is_equal(source[key], target[key])) {
                    return false;
                }
            }
        }
        return true;
    }
    return source === target;
}


const bas64ToBlob = (b64Data: string, mime?: string) => {
    let [prefix, data] = b64Data.split(",");
    let byteArrays = [], sliceSize = 512;
    if (!mime && data) {
        mime = prefix.match(/:(.*?);/)[1];
    }
    let byteChars = window.atob(data || prefix);
    console.log('mime...', mime)
    for (let offset = 0; offset < byteChars.length; offset += sliceSize) {
        let slice = byteChars.slice(offset, offset + sliceSize);
        let byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }
        byteArrays.push(new Uint8Array(byteNumbers));
    }
    return new Blob(byteArrays, {type: mime});
}


const base64ToMp3 = (b64Data: string, filename?: string) => {
    let blob = bas64ToBlob(b64Data, "audio/mpeg");
    return new File([blob], filename || `${uuid()}.mp3`);
}

const imageUrl2Base64 = async (url: string) => {
    return new Promise<string>((resolve, reject) => {
        let img = document.createElement('img');
        img.crossOrigin = "anonymous"
        img.onload = () => {
            let canvas = document.createElement("canvas");
            canvas.width = img.width;
            canvas.height = img.height;
            let ctx = canvas.getContext("2d");
            if (ctx) {
                ctx.drawImage(img, 0, 0);
                let dataURL = canvas.toDataURL("image/png");
                canvas.remove();
                img.remove();
                resolve(dataURL);
            }
            canvas.remove();
            img.remove();
        }
        img.onerror = (e: any) => {
            console.error('image load failed...', e);
            img.remove();
            reject(e);
        }
        img.src = url;
    })
}

const MediaHelper = {
    video_size: async (src: string) => {
        src = (await UpyunAccess.getSign(src)) as string;
        return new Promise<{width: number, height: number}>((resolve, reject) => {
            let video = document.createElement('video');
            video.onloadedmetadata = () =>  {
                let width = video.videoWidth;
                let height = video.videoHeight;
                video.remove();
                resolve({ width, height });
            }
            video.onerror = (e: any) => {
                console.error('video load failed...', e);
                video.remove();
                reject(e);
            }
            video.src = src;
        })
    },
    load_video: async (src: string) => {
        return new Promise<{width: number, height: number}>((resolve, reject) => {
            let video = document.createElement('video');
            video.onloadedmetadata = () =>  {
                video.remove();
                resolve(src);
            }
            video.onerror = (e: any) => {
                console.error('video load failed...', src);
                video.remove();
                reject(e);
            }
            video.src = src;
        })
    },
    get_video_url: async (src: string) => {
        if (!src) return ;
        let url = (await UpyunAccess.getSign(src)) as string;
        try {
            await MediaHelper.load_video(url)
            return url;
        } catch (e) {
            console.log("load failed, try compressed path", e)
            if (!src.includes("_compressed")) {
                let paths = src.split(".");
                let ext = paths.pop();
                url = (await UpyunAccess.getSign(paths.join(".")  + "_compressed." + ext)) as string;
                try {
                    await MediaHelper.load_video(url)
                    return url;
                } catch (e) {
                    console.log("load compressed failed...", url, src)
                }
            }
        }
    },
    authorize: async ({resource_url, url, ...rest}: Partial<RefResource & UploadPhoto>) => {
        if (resource_url) {
            return {
                ...rest, resource_url: (await UpyunAccess.getSign(resource_url))
            }
        }
        return {
            ...rest, url: (await UpyunAccess.getSign(url))
        }
    },
    audio_transform: function (audio?: RefResource) {
        let audio_url = audio?.resource_url, media_id: string | undefined;
        if (audio?.tasks) {
            if (audio.tasks.vals?.length) {
                media_id = audio.tasks.vals[0]?.task_meta?.media_id
            }
        }
        return { audio_url, media_id };
    },
    watermark: async (url: string, user_id: string) => {
        const { r, d, e } = await request({
            url: "/api/v1/ai/watermark", method: "POST",
            headers: {Authorization: 'Basic aHVpd2luZzo5dm9EOVNSMVNMS3h3SnhNeVFNVQ=='},
            data: {video_url: url, user_id},
        });
        console.log(r, d, e)
    },
    remove: async (resources?: RefResource[]) => {
        try {
            let ids = resources?.map(r => r.hty_resource_id).filter(x => !!x)
            // remove hty resource
            await Promise.all(ids.map(x => request({url: `/api/v1/uc/delete_hty_resource_by_id/${x}`})));
        } catch (e) {
            console.log("delete resource failed...", e)
        }
    }
}

const AiTaskHelper = {
    mapStatus: (ai_task_status: AiTaskStatuses): TaskStatuses => {
        switch(ai_task_status) {
            case AiTaskStatuses.PENDING:
                return TaskStatuses.PENDING;
            case AiTaskStatuses.RECEIVED:
            case AiTaskStatuses.STARTED:
                return TaskStatuses.PROCESSING;
            case AiTaskStatuses.SUCCESS:
                return TaskStatuses.DONE;
            case AiTaskStatuses.FAILED:
                return TaskStatuses.FAILED;
            default:
                return TaskStatuses.UNKNOWN;
        }
    },
    parseRes: ({r, d, e, statusCode}: any, start_time: string | Date, task_type: TaskTypes): CommonTask => {
        if (r) {
            if (task_type === TaskTypes.AI_SCORE) {
                return AiTaskHelper.parseScoreData(d)
            } else if (task_type === TaskTypes.WATERMARK) {
                return AiTaskHelper.parseWatermarkData(d)
            }
        }
        let { task_id, task_status} = d;
        let data: CommonTask = {
            task_id, task_type,
            task_status: AiTaskHelper.mapStatus(task_status),
        };
        if (e) {
            // check if network error
            if (statusCode !== 200) {
                // 404 两种，1. task不存在，2. task刚创建，在redis里不存在
                if (statusCode === 404) {

                    // 任务创建超过30秒还未找到，则认为是task不存在
                    if (Date.now() - new Date(start_time).valueOf() > 30 * 1000) {
                        data.task_status = TaskStatuses.FAILED;
                        let error;
                        if (typeof e === 'object') {
                            let { status, ...rest } = e;
                            error = JSON.stringify(rest)
                        }  else {
                            error = e;
                        }
                        data.task_meta = {data: {error}}
                    }
                }
            } else {
                // 非网络异常，设为失败
                data.task_status = TaskStatuses.FAILED;
                let error;
                if (typeof e === 'object') {
                    let { status, ...rest } = e;
                    error = JSON.stringify(rest)
                }  else {
                    error = e;
                }
                data.task_meta = {data: {error}}
            }
        }
        return data;

    },
    parseScoreData: ({task_id, task_result, task_status}: any): CommonTask => {
        let data: CommonTask = {
            task_id, task_type: TaskTypes.AI_SCORE,
            task_status: AiTaskHelper.mapStatus(task_status)
        };

        if (task_status === AiTaskStatuses.SUCCESS) {
            let { accuracy, recall, diff } = task_result;
            data.task_result = {accuracy: accuracy.toString(), recall: recall.toString()}
            data.task_meta = {data: {diff: JSON.stringify(diff)}};
        } else {
            if (task_result) {
                let { status, ...rest } = task_result
                Object.entries(rest).forEach(([k, v]) => rest[k] = JSON.stringify(v))
                data.task_meta = {data: rest}
            }
        }
        return data;
    },
    parseWatermarkData: ({task_id, task_result, task_status}: any): CommonTask => {
        let data: CommonTask = {
            task_id, task_type: TaskTypes.WATERMARK,
            task_status: AiTaskHelper.mapStatus(task_status)
        };

        if (task_status === AiTaskStatuses.SUCCESS) {
            const tr = task_result || {};
            const url = tr.watermarked_video_url ?? tr.marked_url;
            if (url) {
                data.task_result = { watermarked_video_url: url };
            }
        } else {
            if (task_result) {
                let { status, ...rest } = task_result
                Object.entries(rest).forEach(([k, v]) => rest[k] = JSON.stringify(v))
                data.task_meta = {data: rest}
            }
        }
        return data;
    }
}

const get_search_params = (url?: string)  => {
    let params: any = {};
    let pairs = ((url || window.location.href).split("?")[1] || "").split("&");
    pairs.forEach(pair => {
        let [k, v] = pair.split("=")
        // @ts-ignore
        if (v === "undefined") v = undefined;
        // @ts-ignore
        if (v === "null") v = null;

        let tmp = params[k];
        if (tmp) {
            // handle array params case
            if (!(tmp instanceof Array)) {
                tmp = [tmp]
            }
            tmp.push(v)
            params[k] = tmp;
        } else {
            params[k] = v;
        }
    })
    return params
}

const isCommentsDifferent = (a: Comment[], b: Comment[]) => {
    let valA = a || [], valB = b || [];
    if (valA.length !== valB.length) return true;
    return valA.some(x => !valB.some(y => !isCommentDifferent(x, y)))
}

const commentContentShrink = (content: CommentContent) => {
    // for these medias, if id exists, means this media is in ref_resources of comment
    // remove the one in content for easier compare
    if (content?.audio?.id) {
        delete content.audio
    }
    if (content?.video?.id) {
        delete content.video
    }
    if (content?.photos?.length) {
        content.photos = content.photos.filter(p => !p.id)
    }
}

const isCommentDifferent = (a: Comment, b: Comment) => {
    if (!a) return !!b;
    if (!b) return true;
    let { content: contentA, ...restA} = a;
    let { content: contentB, ...restB } = b;
    commentContentShrink(contentA)
    commentContentShrink(contentB)
    if (!is_equal(restA, restB)) return true;
    let keys = [
        {name: "text", defaultVal: undefined},
        {name: "meta", defaultVal: undefined},
        {name: "audio", defaultVal: null},
        {name: "video", defaultVal: null},
        {name: "photos", defaultVal: []}
    ];
    for (let i = 0; i < keys.length; i++) {
        let {name, defaultVal} = keys[i];
        let valA = contentA[name] || defaultVal,
            valB = contentB[name] || defaultVal;
        if (!is_equal(valA, valB)) return true;
    }
    return false;
}

const isQupuMarksChanged = (prev: string, cur: string, check_comment = true) => {
    let prev_marks = JSON.parse(prev), cur_marks = JSON.parse(cur);
    if (prev_marks.length !== cur_marks.length) {
        return true;
    }
    for(let i = 0; i < cur_marks.length; i++) {
        let c = cur_marks[i];
        let p = prev_marks.find((x: ResourceNote) => c.serial_no === x.serial_no);
        if (!p) return true;
        if (c.marker?._leaflet_id !== p.marker?._leaflet_id) {
            return true;
        }
        let keys = ["id", "createdAt", "created_by", "note_type", "resource_pos", "lat_pos", "time_pos"];
        for (let j = 0; j < keys.length; j++) {
            if (!is_equal(p[keys[j]], c[keys[j]])) {
                return true;
            }
        }
        if (isCommentsDifferent(p.comments, c.comments)) return true;
        if (check_comment && isCommentDifferent(p.comment, c.comment)) return true;
    }
    return false;
}

const hexToRgb = (hex) => {
    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
    let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
    hex = hex.replace(shorthandRegex, function(m, r, g, b) {
        return r + r + g + g + b + b;
    });

    let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : null;
}


export { getHtyErrUserMessage } from '~/utils/htyErrMessage';
export {
    formatDate,
    getWeekEnd,
    DateFormatter,
    BaseUrl,
    HtyUnionIDToken,
    HtyAuthToken,
    HtySudoToken,
    WebViewEntryUrl,
    HtyHostHeader,
    CurrentUserRole,
    CurrentOrgId,
    UserStatusKey,
    CurrentTeacherId,
    CurrentUserName,
    PiyueLockMinutes,
    list2indexed,
    uuid,
    pad,
    bas64ToBlob,
    base64ToMp3,
    isIOS,
    ImageHelper,
    isWxEnv,
    MediaHelper,
    AiTaskHelper,
    get_search_params,
    is_equal,
    imageUrl2Base64,
    isQupuMarksChanged,
    hexToRgb
}
