import axios from 'axios';
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
import dataUriToBuffer from "data-uri-to-buffer";
import ImageResize from "image-resize";
import { AutoDiscovery } from 'matrix-js-sdk';
import User from '../models/user';
const prettyBytes = require("pretty-bytes");
import Hammer from "hammerjs";
import { Thread } from 'matrix-js-sdk/lib/models/thread';

export const STATE_EVENT_ROOM_DELETION_NOTICE = "im.keanu.room_deletion_notice";
export const STATE_EVENT_ROOM_DELETED = "im.keanu.room_deleted";

export const ROOM_TYPE_DEFAULT = "im.keanu.room_type_default";
export const ROOM_TYPE_VOICE_MODE = "im.keanu.room_type_voice";
export const ROOM_TYPE_FILE_MODE = "im.keanu.room_type_file";
export const ROOM_TYPE_CHANNEL = "im.keanu.room_type_channel";

export const STATE_EVENT_ROOM_TYPE = "im.keanu.room_type";

const sizeOf = require("image-size");

var dayjs = require('dayjs');
var sha256 = require('js-sha256').sha256;
var aesjs = require('aes-js');
var base64Url = require('json-web-key/lib/base64url');

// Install extended localized format
var localizedFormat = require('dayjs/plugin/localizedFormat')
dayjs.extend(localizedFormat)
var duration = require('dayjs/plugin/duration')
dayjs.extend(duration);

// Store info about getUserMedia BEFORE we aply polyfill(s)!
var _browserCanRecordAudioF = function () {
    var legacyGetUserMedia = (navigator.getUserMedia ||
        navigator.webkitGetUserMedia ||
        navigator.mozGetUserMedia ||
        navigator.msGetUserMedia);
    return legacyGetUserMedia !== undefined || (navigator.mediaDevices && navigator.mediaDevices.getUserMedia !== undefined);
}
var _browserCanRecordAudio = _browserCanRecordAudioF();

class UploadPromise {
    aborted = false;
    onAbort = undefined;

    constructor(wrappedPromise) {
        this.wrappedPromise = wrappedPromise;
    }

    abort() {
        this.aborted = true;
        if (this.onAbort) {
            this.onAbort();
        }
    }

    then(resolve, reject) {
        this.wrappedPromise = this.wrappedPromise.then(resolve, reject);
        return this;
    }

    catch(handler) {
        this.wrappedPromise = this.wrappedPromise.catch(handler);
        return this;
    }
}

class Util {

    threadMessageType() {
        return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread"
    }

    getAttachmentUrlAndDuration(event) {
        return new Promise((resolve, reject) => {
          const content = event.getContent();
          if (content.url != null) {
            resolve([content.url, content.info.duration]);
            return;
          }
          if (content.file && content.file.url) {
            resolve([content.file.url, content.info.duration]);
          } else {
            reject("No url found!");
          }
        });
      }

    getAttachment(matrixClient, event, progressCallback, asBlob = false, abortController = undefined) {
        return new Promise((resolve, reject) => {
            const content = event.getContent();
            if (content.url != null) {
                // Unencrypted, just return!
                resolve(matrixClient.mxcUrlToHttp(content.url));
                return;
            }
            var url = null;
            var file = null;
            if (content.file && content.file.url) {
                file = content.file;
                url = matrixClient.mxcUrlToHttp(file.url);
            }

            if (url == null) {
                reject("No url found!");
            }

            axios.get(url, {
                signal: abortController ? abortController.signal : undefined,
                responseType: 'arraybuffer', onDownloadProgress: progressEvent => {
                    let percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total);
                    if (progressCallback) {
                        progressCallback(percentCompleted);
                    }
                }
            })
                .then(response => {
                    return this.decryptIfNeeded(file, response);
                })
                .then(bytes => {
                    if (asBlob) {
                        resolve(new Blob([bytes.buffer], { type: file.mimetype }));
                    } else {
                        resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
                    }
                })
                .catch(err => {
                    console.log("Download error: ", err);
                    reject(err);
                })
                .finally(() => {
                    if (progressCallback) {
                        progressCallback(null);
                    }
                });
        });
    }

    getThumbnail(matrixClient, event, config, ignoredw, ignoredh) {
        return new Promise((resolve, reject) => {
            const content = event.getContent();
            if (content.url != null) {
                // Unencrypted, just return!
                resolve(matrixClient.mxcUrlToHttp(content.url));
                return;
            }
            var url = null;
            var file = null;
            if (
                content &&
                content.info &&
                content.info.thumbnail_file &&
                content.info.thumbnail_file.url
            ) {
                file = content.info.thumbnail_file;
                // var width = w;
                // var height = h;
                // if (content.info.w < w || content.info.h < h) {
                //     width = content.info.w;
                //     height = content.info.h;
                // }
                // url = matrixClient.mxcUrlToHttp(
                //     file.url,
                //     width, height,
                //     "scale",
                //     true
                // );
                url = matrixClient.mxcUrlToHttp(file.url);
            } else if (content.file && content.file.url && this.getFileSize(event) > 0 && this.getFileSize(event) < config.maxSizeAutoDownloads) {
                // No thumb, use real url
                file = content.file;
                url = matrixClient.mxcUrlToHttp(file.url);
            }

            if (url == null) {
                reject("No url found!");
                return;
            }
            axios.get(url, { responseType: 'arraybuffer' })
                .then(response => {
                    return this.decryptIfNeeded(file, response);
                })
                .then(bytes => {
                    resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: file.mimetype })));
                })
                .catch(err => {
                    console.log("Download error: ", err);
                    reject(err);
                });
        });
    }

    decryptIfNeeded(file, response) {
        return new Promise((resolve, reject) => {
            var key = base64Url.decode(file.key.k);
            var iv = base64Url.decode(file.iv);
            var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));

            const data = new Uint8Array(response.data);

            // Calculate sha256 and compare hashes
            var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());
            const originalHash = base64Url.decode(file.hashes.sha256);
            if (Buffer.compare(Buffer.from(hash), Buffer.from(originalHash.buffer)) != 0) {
                reject("Hashes don't match!");
                return;
            }

            var decryptedBytes = aesCtr.decrypt(data);
            resolve(decryptedBytes);
        });
    }

    sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent) {
        var content = ContentHelpers.makeTextMessage(text);
        if (editedEvent) {
            content['m.relates_to'] = {
                rel_type: 'm.replace',
                event_id: editedEvent.getId()
            }
            content['m.new_content'] = {
                body: content.body,
                msgtype: content.msgtype
            }
        } else if (replyToEvent) {
            content['m.relates_to'] = {
                'm.in_reply_to': {
                    event_id: replyToEvent.getId()
                }
            }
            let senderContent = replyToEvent.getContent()

            const senderContentBody = Object.getOwnPropertyDescriptor(senderContent, 'body') ? senderContent.body : Object.values(senderContent)[0].question.body
            // Prefix the content with reply info (seems to be a legacy thing)
            const prefix = senderContentBody.split('\n').map((item, index) => {
                return "> " + (index == 0 ? ("<" + replyToEvent.getSender() + "> ") : "") + item;
            }).join('\n');
            content.body = prefix + "\n\n" + content.body;
        }
        return this.sendMessage(matrixClient, roomId, "m.room.message", content);
    }

    sendQuickReaction(matrixClient, roomId, emoji, event, extraData = {}) {
        const content = {
            'm.relates_to': Object.assign(extraData, {
                key: emoji,
                rel_type: 'm.annotation',
                event_id: event.getId()
            })
        };
        return this.sendMessage(matrixClient, roomId, "m.reaction", content);
    }

    createPoll(matrixClient, roomId, question, answers, isDisclosed) {
        var idx = 0;
        let answerData = answers.map(a => {
            idx++;
            return { id: "" + idx, 'org.matrix.msc1767.text': a.text }
        });
        const content = {
            'org.matrix.msc3381.poll.start': {
                question: {
                    'org.matrix.msc1767.text': question,
                    body: question
                },
                kind: isDisclosed ? "org.matrix.msc3381.poll.disclosed" : "org.matrix.msc3381.poll.undisclosed",
                max_selections: 1,
                answers: answerData
            },
        };
        return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.start", content);
    }

    closePoll(matrixClient, roomId, event) {
        const content = {
            'm.relates_to': {
                rel_type: 'm.reference',
                event_id: event.getId()
            },
            'org.matrix.msc3381.poll.end': {
            },
        };
        return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.end", content);
    }

    sendPollAnswer(matrixClient, roomId, answers, event) {
        const content = {
            'm.relates_to': {
                rel_type: 'm.reference',
                event_id: event.getId()
            },
            'org.matrix.msc3381.poll.response': {
                answers: answers
            },
        };
        return this.sendMessage(matrixClient, roomId, "org.matrix.msc3381.poll.response", content);
    }

    sendMessage(matrixClient, roomId, eventType, content) {
        return new Promise((resolve, reject) => {
            matrixClient.sendEvent(roomId, eventType, content, undefined, undefined)
                .then((result) => {
                    console.log("Message sent: ", result);
                    resolve(result.event_id);
                })
                .catch(err => {
                    console.log("Send error: ", err);
                    if (err && err.name == "UnknownDeviceError") {
                        console.log("Unknown devices. Mark as known before retrying.");
                        var setAsKnownPromises = [];
                        for (var user of Object.keys(err.devices)) {
                            const userDevices = err.devices[user];
                            for (var deviceId of Object.keys(userDevices)) {
                                const deviceInfo = userDevices[deviceId];
                                if (!deviceInfo.known) {
                                    setAsKnownPromises.push(
                                        matrixClient.setDeviceKnown(
                                            user,
                                            deviceId,
                                            true
                                        )
                                    );
                                }
                            }
                        }
                        Promise.all(setAsKnownPromises)
                            .then(() => {
                                // All devices now marked as "known", try to resend
                                let event = err.event;
                                if (!event) {
                                    // Seems event is no longer send in the UnknownDevices error...
                                    const room = matrixClient.getRoom(roomId);
                                    if (room) {
                                        event = room.getLiveTimeline().getEvents().find(e => {
                                            // Find the exact match (= object equality)
                                            return e.error === err
                                        });
                                    }
                                }
                                matrixClient.resendEvent(event, matrixClient.getRoom(event.getRoomId()))
                                    .then((result) => {
                                        console.log("Message sent: ", result);
                                        resolve(result.event_id);
                                    })
                                    .catch((err) => {
                                        // Still error, abort
                                        reject(err.toLocaleString());
                                    });
                            });
                    }
                    else {
                        reject(err.toLocaleString());
                    }
                });
        });
    }

    sendFile(matrixClient, roomId, file, onUploadProgress, threadRoot) {
        const uploadPromise = new UploadPromise(undefined);
        uploadPromise.wrappedPromise = new Promise((resolve, reject) => {
            var reader = new FileReader();
            reader.onload = (e) => {
                if (uploadPromise.aborted) {
                    reject("Aborted");
                    return;
                }
                const fileContents = e.target.result;
                var data = new Uint8Array(fileContents);

                const info = {
                    mimetype: file.type,
                    size: file.size
                };

                // If audio, send duration in ms as well
                if (file.duration) {
                    info.duration = file.duration;
                }

                var description = file.name;
                var msgtype = 'm.file';
                if (file.type.startsWith("image/")) {
                    msgtype = 'm.image';
                } else if (file.type.startsWith("audio/")) {
                    msgtype = 'm.audio';
                } else if (file.type.startsWith("video/")) {
                    msgtype = 'm.video';
                }

                const opts = {
                    type: file.type,
                    name: description,
                    progressHandler: onUploadProgress,
                    onlyContentUri: false
                };

                var messageContent = {
                    body: description,
                    info: info,
                    msgtype: msgtype
                }

                // If thread root (an eventId) is set, add that here
                if (threadRoot) {
                    messageContent["m.relates_to"] = {
                        "rel_type":  this.threadMessageType(),
                        "event_id": threadRoot
                    };
                }

                // Set filename for files
                if (msgtype == 'm.file') {
                    messageContent.filename = file.name;
                }

                if (!matrixClient.isRoomEncrypted(roomId)) {
                    // Not encrypted.
                    const promise = matrixClient.uploadContent(data, opts);
                    uploadPromise.onAbort = () => {
                        matrixClient.cancelUpload(promise);
                    };
                    promise
                        .then((response) => {
                            messageContent.url = response.content_uri;
                            return (msgtype == 'm.audio' ? this.generateWaveform(fileContents, messageContent) : true);
                        })
                        .then(() => {
                            return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
                        })
                        .then(result => {
                            resolve(result);
                        })
                        .catch(err => {
                            reject(err);
                        });
                    return; // Don't fall through
                }

                const crypto = require('crypto');
                let key = crypto.randomBytes(256 / 8);
                let iv = Buffer.concat([crypto.randomBytes(8), Buffer.alloc(8)]); // Initialization vector.

                // Encrypt
                var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
                var encryptedBytes = aesCtr.encrypt(data);
                data = encryptedBytes;

                // Calculate sha256
                var hash = new Uint8Array(sha256.create().update(data).arrayBuffer());

                const jwk = {
                    kty: 'oct',
                    key_ops: ['encrypt', 'decrypt'],
                    alg: 'A256CTR',
                    k: base64Url.encode(key),
                    ext: true
                };

                const encryptedFile = {
                    mimetype: file.type,
                    key: jwk,
                    iv: Buffer.from(iv).toString('base64').replace(/=/g, ''),
                    hashes: { sha256: Buffer.from(hash).toString('base64').replace(/=/g, '') },
                    v: 'v2'
                };

                messageContent.file = encryptedFile;

                // Encrypted data sent as octet-stream!
                opts.type = "application/octet-stream";

                const promise = matrixClient.uploadContent(data, opts);
                uploadPromise.onAbort = () => {
                    matrixClient.cancelUpload(promise);
                };
                promise
                .then((response) => {
                        if (response.error) {
                            return reject(response.error);
                        }
                        encryptedFile.url = response.content_uri;
                        return (msgtype == 'm.audio' ? this.generateWaveform(fileContents, messageContent) : true);
                    })
                    .then(() => {
                        return this.sendMessage(matrixClient, roomId, "m.room.message", messageContent)
                    })
                    .then(result => {
                        resolve(result);
                    })
                    .catch(err => {
                        reject(err);
                    });
            }
            reader.onerror = (err) => {
                reject(err);
            }
            reader.readAsArrayBuffer(file);
        });
        return uploadPromise;
    }

    generateWaveform(data, messageContent) {
        if (!(window.AudioContext || window.webkitAudioContext)) {
            return; // No support
        }
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        if (audioCtx) {
            return audioCtx.decodeAudioData(data)
                .then((audioBuffer) => {
                    const rawData = audioBuffer.getChannelData(0); // TODO - currently using only 1 channel
                    const samples = 1000; // Number of samples
                    const blockSize = Math.floor(rawData.length / samples);
                    let filteredData = [];
                    for (let i = 0; i < samples; i++) {
                      let blockStart = blockSize * i; // the location of the first sample in the block
                      let sum = 0;
                      for (let j = 0; j < blockSize; j++) {
                        sum = sum + Math.abs(rawData[blockStart + j]) // find the sum of all the samples in the block
                      }
                      filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
                    }

                    // Normalize
                    const multiplier = Math.pow(Math.max(...filteredData), -1);
                    filteredData = filteredData.map(n => n * multiplier);

                    // Integerize
                    filteredData = filteredData.map(n => parseInt((n * 255).toFixed()));
                    

                    // Generate SVG of waveform
                    let svg = `<svg viewBox="0 0 ${samples} 255" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">`;
                    svg += `<path d="`;
                    filteredData.forEach((d, i) => {
                        const delta = d / 2; 
                        svg += `M${i} ${128 - delta}V${128 + delta}`;
                    });
                    svg += `" style="fill:none;stroke:green;stroke-width:1" />`;
                    svg += "</svg>";

                    messageContent.format = "org.matrix.custom.html";
                    messageContent.formatted_body = svg;
                })
        }
    }

    /**
     * Return what "mode" to use for the given room.
     *
     * The default value is given by the room itself (as state events, see roomTypeMixin).
     * This method just returns if the user has overridden this in room settings (this
     * fact will be persisted as a user specific tag on the room). Note: currently override
     * is disabled in the UI...
     */
    roomDisplayTypeOverride(roomOrNull) {
        if (roomOrNull) {
            const room = roomOrNull;

            // Have we changed our local view mode of this room?
            const tags = room.tags;
            if (tags && tags["ui_options"]) {
                console.error("We have a tag!");
                if (tags["ui_options"]["voice_mode"] === 1) {
                    return ROOM_TYPE_VOICE_MODE;
                } else if (tags["ui_options"]["file_mode"] === 1) {
                    return ROOM_TYPE_FILE_MODE;
                } else if (tags["ui_options"]["file_mode"] === 0 && tags["ui_options"]["file_mode"] === 0) {
                    // Explicitly set to "default"
                    return ROOM_TYPE_DEFAULT;
                }
            }
        }
        return null;
    }

    /**
     * Return the room type for the current room
     * @param {*} roomOrNull
     */
    roomDisplayTypeToQueryParam(roomOrNull, roomDisplayType) {
        const roomType = this.roomDisplayTypeOverride(roomOrNull) || roomDisplayType;
        if (roomType === ROOM_TYPE_FILE_MODE) {
            // Send "file" here, so the receiver of the invite link knows to display the "file drop" join page
            // instead of the standard one.
            return "file";
        } else if (roomType === ROOM_TYPE_VOICE_MODE) {
            // No need to return "voice" here. The invite page looks the same for default and voice mode,
            // so currently no point in cluttering the invite link with it. The corrent mode will be picked up from
            // room creation flags once the user joins.
            return undefined;
        }
        return undefined; // Default, just return undefined
    }

    /** Generate a random user name */
    randomUser(prefix) {
        var pfx = prefix ? prefix.replace(/[^0-9a-zA-Z\-_]/gi, '') : null;
        if (!pfx || pfx.length == 0) {
            pfx = "weblite-";
        }
        return pfx + this.randomString(
            12,
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
        );
    }

    /**
     * Generate random 12 char password
     */
    randomPass() {
        return this.randomString(
            12,
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#_-*+"
        );
    }

    // From here: https://stackoverflow.com/questions/1349404/generate-random-string-characters-in-javascript
    randomString(length, characters) {
        var result = "";
        var charactersLength = characters.length;
        for (var i = 0; i < length; i++) {
            result += characters.charAt(
                Math.floor(Math.random() * charactersLength)
            );
        }
        return result;
    }

    sanitizeRoomId(roomId) {
        if (roomId && roomId.match(/^(!|#).+$/)) {
            return roomId;
        }
        return null;
    }

    sanitizeUserId(userId) {
        if (userId && userId.match(/^([0-9a-z-.=_/]+|@[0-9a-z-.=_/]+:.+)$/)) {
            return userId;
        }
        return null;
    }

    invalidUserIdChars() {
        return /[^0-9a-z-.=_/]+/g;
    }

    getFirstVisibleElement(parentNode, where) {
        let visible = this.findVisibleElements(parentNode);
        if (visible) {
            visible = visible.filter(where);
        }
        if (visible && visible.length > 0) {
            return visible[0];
        }
        return null;
    }

    getLastVisibleElement(parentNode, where) {
        let visible = this.findVisibleElements(parentNode);
        if (visible) {
            visible = visible.filter(where);
        }
        if (visible && visible.length > 0) {
            return visible[visible.length - 1];
        }
        return null;
    }

    findVisibleElements(parentNode) {
        const middle = this.findOneVisibleElement(parentNode);
        if (middle) {
            var nodes = [parentNode.children[middle]];
            var i = middle - 1;
            while (i >= 0 && this.isChildVisible(parentNode, parentNode.children[i])) {
                nodes.splice(0, 0, parentNode.children[i]);
                i -= 1;
            }
            i = middle + 1;
            while (i < parentNode.children.length && this.isChildVisible(parentNode, parentNode.children[i])) {
                nodes.push(parentNode.children[i]);
                i += 1;
            }
            return nodes;
        }
        return null; // No visible found
    }

    isChildVisible(parentNode, childNode) {
        const rect1 = parentNode.getBoundingClientRect();
        const rect2 = childNode.getBoundingClientRect();
        var overlap = !(rect1.right <= rect2.left ||
            rect1.left >= rect2.right ||
            rect1.bottom <= rect2.top ||
            rect1.top >= rect2.bottom)
        return overlap;
    }

    findOneVisibleElement(parentNode) {
        let start = 0;
        let end = (parentNode && parentNode.children) ? parentNode.children.length - 1 : -1;
        while (start <= end) {
            let middle = Math.floor((start + end) / 2);
            let childNode = parentNode.children[middle];
            if (this.isChildVisible(parentNode, childNode)) {
                // found the key
                return middle;
            } else if (childNode.getBoundingClientRect().top <= parentNode.getBoundingClientRect().top) {
                // continue searching to the right
                start = middle + 1;
            } else {
                // search searching to the left
                end = middle - 1;
            }
        }
        // key wasn't found
        return null;
    }

    _importAll(r) {
        var images = [];
        r.keys().forEach(res => {
            console.log("Avatar", res);
            // // Remove"./"
            var name = res.split("_")[1];
            name = name.slice(0, name.indexOf("."));
            name = name.charAt(0).toUpperCase() + name.slice(1);
            const image = r(res);
            const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
            images.push({ id: res, image: image, name: "Guest " + name + " " + randomNumber});
        });
        return images;
    }

    getDefaultAvatars() {
        return this._importAll(require.context('../assets/avatars/', true, /\.(jpeg|jpg|png)$/));
    }

    setAvatar(matrix, file, onUploadProgress) {
        return new Promise((resolve, reject) => {
            axios.get(file, { responseType: 'arraybuffer' })
                .then(response => {
                    const opts = {
                        type: response.headers['content-type'].split(';')[0],
                        name: "Avatar",
                        progressHandler: onUploadProgress,
                        onlyContentUri: false
                    };
                    var avatarUri;
                    matrix.matrixClient.uploadContent(response.data, opts)
                        .then((response) => {
                            avatarUri = response.content_uri;
                            return matrix.matrixClient.setAvatarUrl(avatarUri);
                        })
                        .then(result => {
                            matrix.userAvatar = avatarUri;
                            resolve(result);
                        })
                        .catch(err => {
                            reject(err);
                        });
                })
                .catch(err => {
                    reject(err);
                });

        })
    }

    setRoomAvatar(matrixClient, roomId, file, onUploadProgress) {
        return new Promise((resolve, reject) => {
            var reader = new FileReader();
            reader.onload = (e) => {
                const fileContents = e.target.result;
                var data = new Uint8Array(fileContents);

                const info = {
                    mimetype: file.type,
                    size: file.size
                };

                const opts = {
                    type: file.type,
                    name: "Room Avatar",
                    progressHandler: onUploadProgress,
                    onlyContentUri: false
                };

                var messageContent = {
                    body: file.name,
                    info: info
                }

                matrixClient.uploadContent(data, opts)
                    .then((response) => {
                        messageContent.url = response.content_uri;
                        return matrixClient.sendStateEvent(roomId, "m.room.avatar", messageContent);
                    })
                    .then(result => {
                        resolve(result);
                    })
                    .catch(err => {
                        reject(err);
                    });
            }
            reader.onerror = (err) => {
                reject(err);
            }
            reader.readAsArrayBuffer(file);
        });
    }

    loadAvatarFromFile(event, onLoad) {
        if (event.target.files && event.target.files[0]) {
            var reader = new FileReader();
            reader.onload = (e) => {
                const file = event.target.files[0];
                if (file.type.startsWith("image/")) {
                    try {
                        var image = e.target.result;

                        var dimens = sizeOf(dataUriToBuffer(e.target.result));

                        // Need to resize?
                        const w = dimens.width;
                        const h = dimens.height;
                        if (w > 640 || h > 640) {
                            var aspect = w / h;
                            var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
                            var newHeight = parseInt(
                                (w > h ? 640 / aspect : 640).toFixed()
                            );
                            var imageResize = new ImageResize({
                                format: "png",
                                width: newWidth,
                                height: newHeight,
                                outputType: "blob",
                            });
                            imageResize
                                .play(event.target)
                                .then((img) => {
                                    var resizedImageFile = new File([img], file.name, {
                                        type: img.type,
                                        lastModified: Date.now(),
                                    });
                                    var reader2 = new FileReader();
                                    reader2.onload = (e) => {
                                        onLoad(e.target.result);
                                    };
                                    reader2.readAsDataURL(resizedImageFile);
                                })
                                .catch((err) => {
                                    console.error("Resize failed:", err);
                                });
                        } else {
                            onLoad(image);
                        }
                    } catch (error) {
                        console.error("Failed to get image dimensions: " + error);
                    }
                }
            };
            reader.readAsDataURL(event.target.files[0]);
        }
    }

    /**
     * Return number of whole days between the timestamps, at end of that day.
     * @param {*} ts1
     * @param {*} ts2
     */
    dayDiff(ts1, ts2) {
        var t1 = dayjs(ts1).endOf('day');
        var t2 = dayjs(ts2).endOf('day');
        return t2.diff(t1, 'day');
    }

    dayDiffToday(timestamp) {
        var then = dayjs(timestamp).endOf('day');
        var now = dayjs().endOf('day');
        return now.diff(then, 'day');
    }

    formatDay(timestamp) {
        var then = dayjs(timestamp).endOf('day');
        return then.format('L');
    }

    formatTime(time) {
        const date = new Date();
        date.setTime(time);

        const today = new Date();
        if (
            date.getDate() == today.getDate() &&
            date.getMonth() == today.getMonth() &&
            date.getFullYear() == today.getFullYear()
        ) {
            // For today, skip the date part
            return date.toLocaleTimeString();
        }
        return date.toLocaleString();
    }

    formatRecordDuration(ms) {
        return dayjs.duration(ms).format("HH:mm:ss");
    }

    formatDuration(ms) {
        if (ms >= (60 * 60000)) {
            return dayjs.duration(ms).format("H:mm:ss");
        }
        return dayjs.duration(ms).format("m:ss");
    }

    formatRecordStartTime(timestamp) {
        var then = dayjs(timestamp);
        return then.format('lll');
    }

    browserCanRecordAudio() {
        return _browserCanRecordAudio;
    }

    getRoomNameFromAlias(alias) {
        if (alias && alias.startsWith('#') && alias.indexOf(':') > 0) {
            return alias.slice(1).split(':')[0];
        }
        return undefined;
    }

    getUniqueAliasForRoomName(matrixClient, roomName, defaultMatrixDomainPart, iterationCount) {
        return new Promise((resolve, reject) => {
            var preferredAlias = roomName.replace(/\s/g, "").toLowerCase();
            var tryAlias = "#" + preferredAlias + ":" + defaultMatrixDomainPart;
            matrixClient.getRoomIdForAlias(tryAlias)
                .then(ignoredid => {
                    // We got a response, this means the tryAlias already exists.
                    // Try again, with appended random chars
                    if (iterationCount) {
                        // Not the first time around. Reached max tries?
                        if (iterationCount == 5) {
                            reject("Failed to get unique room alias");
                            return;
                        }
                        // Else, strip random chars from end so we can try again
                        roomName = roomName.substring(0, roomName.length - 5);
                    }
                    const randomChars = this.randomString(4, "abcdefghijklmnopqrstuvwxyz0123456789");
                    resolve(this.getUniqueAliasForRoomName(matrixClient, roomName + "-" + randomChars, defaultMatrixDomainPart, (iterationCount || 0) + 1))
                })
                .catch(err => {
                    if (err.errcode == 'M_NOT_FOUND') {
                        resolve(preferredAlias);
                    } else {
                        reject(err);
                    }
                })
        });
    }

    downloadableTypes() {
        return ['m.video','m.audio','m.image','m.file'];
    }

    download(matrixClient, event) {
        console.log("DOWNLOAD");
        this
        .getAttachment(matrixClient, event)
        .then((url) => {
          const link = document.createElement("a");
          link.href = url;
          link.target = "_blank";
          if (!this.isFileTypePDF(event)) {
            // PDFs are shown inline, not downloaded
            link.download = event.getContent().body || this.$t("fallbacks.download_name");
          }
          console.log("LINK", link);
          document.body.appendChild(link);
          link.click();
          setTimeout(function () {
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
          }, 200);
        })
        .catch((err) => {
          console.log("Failed to fetch attachment: ", err);
        });
    }

    getMatrixBaseUrl(user, config) {
        if (user) {
            const domain = User.domainPart(user.user_id);
            if (domain) {
                const endpoint = config.getMatrixDomainPartMapping(domain);
                if (endpoint) {
                    console.log("Mapped to", endpoint);
                    return Promise.resolve(endpoint);
                }
                return AutoDiscovery.findClientConfig(domain)
                    .then((clientConfig) => {
                        const hs = clientConfig['m.homeserver'];
                        if (hs && !hs.error && hs.base_url) {
                            console.log("Use home server returned from well-known", hs.base_url);
                            return hs.base_url;
                        }
                        console.log("Fallback to default server");
                        return config.defaultBaseUrl;
                    })
                    .catch((err) => {
                        console.error("Failed well-known lookup", err);
                        return config.defaultBaseUrl;
                    });
            }
        }
        return Promise.resolve(config.defaultBaseUrl);
    }

    getMimeType(event) {
        const content = event.getContent();
        return (content.info && content.info.mimetype) ? content.info.mimetype : (content.file && content.file.mimetype) ? content.file.mimetype : "";
    }

    getFileName(event) {
        const content = event.getContent();
        return (content.body || content.filename || "").toLowerCase();
    }

    getFileExtension(event) {
        const fileName = this.getFileName(event);
        const parts = fileName.split(".");
        if (parts.length > 1) {
            return "." + parts[parts.length - 1].toLowerCase();
        }
        return "";
    }

    getFileSize(event) {
        const content = event.getContent();
        if (content.info) {
            return content.info.size;
        }
        return 0;
    }

    getFileSizeFormatted(event) {
        return prettyBytes(this.getFileSize(event));
    }

    isFileTypeAPK(event) {
        const mime = this.getMimeType(event);
        if (mime === "application/vnd.android.package-archive" || this.getFileName(event).endsWith(".apk")) {
            return true;
        }
        return false;
    }

    isFileTypeIPA(event) {
        if (this.getFileName(event).endsWith(".ipa")) {
            return true;
        }
        return false;
    }

    isFileTypePDF(event) {
        const mime = this.getMimeType(event);
        if (mime === "application/pdf" || this.getFileName(event).endsWith(".pdf")) {
            return true;
        }
        return false;
    }

    isFileTypeZip(event) {
        const mime = this.getMimeType(event);
        if (["application/zip", "application/x-zip-compressed", "multipart/x-zip"].includes(mime) || this.getFileName(event).endsWith(".zip")) {
            return true;
        }
        return false;
    }

    isMobileOrTabletBrowser() {
        // Regular expression to match common mobile and tablet browser user agent strings
        const mobileTabletPattern = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Tablet|Mobile|CriOS/i;
        const userAgent = navigator.userAgent;
        return mobileTabletPattern.test(userAgent);
    }

    singleOrDoubleTabRecognizer(element) {
        // reference: https://codepen.io/jtangelder/pen/xxYyJQ
        const hm = new Hammer.Manager(element);

        hm.add(new Hammer.Tap({ event: 'doubletap', taps: 2 }));
        hm.add(new Hammer.Tap({ event: 'singletap' }) );

        hm.get('doubletap').recognizeWith('singletap');
        hm.get('singletap').requireFailure('doubletap');
        return hm
    }
}
export default new Util();

