import axios from "axios";
import * as ContentHelpers from "matrix-js-sdk/lib/content-helpers";
import imageResize from "image-resize";
import { AutoDiscovery, Method } from "matrix-js-sdk";
import User from "../models/user";
import prettyBytes from "pretty-bytes";
import { Thread } from "matrix-js-sdk/lib/models/thread";
import { imageSize } from "image-size";
import dayjs from "dayjs";
import aesjs from "aes-js";
import localizedFormat from "dayjs/plugin/localizedFormat";
import duration from "dayjs/plugin/duration";
import i18n from "./lang";
import { toRaw, isRef, isReactive, isProxy } from "vue";
import { UploadPromise } from "../models/attachment";
import png from "@/plugins/png.ts";
import { mediaMetadataToMediaInterventionFlags } from "../models/proof";

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";
export const CLIENT_EVENT_MEDIA_INTERVENTION_FLAGS = "im.keanu.proof_hint";

export const THUMBNAIL_MAX_WIDTH = 640;
export const THUMBNAIL_MAX_HEIGHT = 640;

// Install extended localized format
dayjs.extend(localizedFormat);
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 Util {
  threadMessageType() {
    return Thread.hasServerSideSupport ? "m.thread" : "io.element.thread";
  }

  getAttachment(matrixClient, useAuthedMedia, event, progressCallback, asBlob = false, abortController = undefined) {
    return new Promise((resolve, reject) => {
      const content = event.getContent();
      var url = null;
      var mime = "image/png";
      var file = null;
      let decrypt = true;
      if (content.url != null) {
        url = matrixClient.mxcUrlToHttp(
          content.url,
          undefined,
          undefined,
          undefined,
          undefined,
          undefined,
          useAuthedMedia
        );
        decrypt = false;
        if (content.info) {
          mime = content.info.mimetype;
        }
      } else if (content.file && content.file.url) {
        file = content.file;
        url = matrixClient.mxcUrlToHttp(
          file.url,
          undefined,
          undefined,
          undefined,
          undefined,
          undefined,
          useAuthedMedia
        );
        mime = file.mimetype;
      }

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

      axios
        .get(url, {
          headers: {
            Authorization: `Bearer ${matrixClient.getAccessToken()}`,
          },
          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 decrypt ? this.decryptIfNeeded(file, response) : Promise.resolve({ buffer: response.data });
        })
        .then((bytes) => {
          if (asBlob) {
            resolve(new Blob([bytes.buffer], { type: mime }));
          } else {
            resolve(URL.createObjectURL(new Blob([bytes.buffer], { type: mime })));
          }
        })
        .catch((err) => {
          console.log("Download error: ", err);
          reject(err);
        })
        .finally(() => {
          if (progressCallback) {
            progressCallback(null);
          }
        });
    });
  }

  async getThumbnail(matrixClient, useAuthedMedia, event, config, ignoredw, ignoredh) {
    const content = event.getContent();
    var url = null;
    var mime = "image/png";
    var file = null;
    let decrypt = true;
    if (!!content.info && !!content.info.thumbnail_url) {
      url = matrixClient.mxcUrlToHttp(
        content.info.thumbnail_url,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        useAuthedMedia
      );
      decrypt = false;
      if (content.info.thumbnail_info) {
        mime = content.info.thumbnail_info.mimetype;
      }
    } else if (content.url != null) {
      url = matrixClient.mxcUrlToHttp(
        content.url,
        undefined,
        undefined,
        undefined,
        undefined,
        undefined,
        useAuthedMedia
      );
      decrypt = false;
      if (content.info) {
        mime = content.info.mimetype;
      }
    } else if (content && content.info && content.info.thumbnail_file && content.info.thumbnail_file.url) {
      file = content.info.thumbnail_file;
      url = matrixClient.mxcUrlToHttp(file.url, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
      mime = file.mimetype;
    } 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, undefined, undefined, undefined, undefined, undefined, useAuthedMedia);
      mime = file.mimetype;
    }

    if (url == null) {
      throw new Error("No url found!");
    }

    const response = await axios.get(
      url,
      useAuthedMedia
        ? {
            responseType: "arraybuffer",
            headers: {
              Authorization: `Bearer ${matrixClient.getAccessToken()}`,
            },
          }
        : { responseType: "arraybuffer" }
    );
    const bytes = decrypt ? await this.decryptIfNeeded(file, response) : { buffer: response.data };
    return URL.createObjectURL(new Blob([bytes.buffer], { type: mime }));
  }

  b64toBuffer(val) {
    const baseValue = val.replaceAll("-", "+").replaceAll("_", "/");
    return Buffer.from(baseValue, "base64");
  }

  decryptIfNeeded(file, response) {
    return new Promise((resolve, reject) => {
      const key = this.b64toBuffer(file.key.k);
      const iv = this.b64toBuffer(file.iv);
      const originalHash = this.b64toBuffer(file.hashes.sha256);

      var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv));
      const data = new Uint8Array(response.data);

      crypto.subtle
        .digest("SHA-256", data)
        .then((hash) => {
          // Calculate sha256 and compare hashes
          if (Buffer.compare(Buffer.from(hash), originalHash) != 0) {
            reject("Hashes don't match!");
            return;
          }
          var decryptedBytes = aesCtr.decrypt(data);
          resolve(decryptedBytes);
        })
        .catch((err) => {
          reject("Failed to calculate hash value");
        });
    });
  }

  sendTextMessage(matrixClient, roomId, text, editedEvent, replyToEvent, txnId) {
    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, txnId);
  }

  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, txnId) {
    return new Promise((resolve, reject) => {
      matrixClient
        .sendEvent(roomId, eventType, content, txnId, 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());
          }
        });
    });
  }

  async encryptFileAndGenerateInfo(data, mime) {
    let key = Buffer.from(crypto.getRandomValues(new Uint8Array(256 / 8)));
    let iv = Buffer.concat([Buffer.from(crypto.getRandomValues(new Uint8Array(8))), Buffer.alloc(8)]); // Initialization vector.

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

    // Calculate sha256
    let hash = await crypto.subtle.digest("SHA-256", encryptedBytes);

    const jwk = {
      kty: "oct",
      key_ops: ["encrypt", "decrypt"],
      alg: "A256CTR",
      k: key.toString("base64").replaceAll(/\//g, "_").replaceAll(/\+/g, "-"),
      ext: true,
    };

    const encryptedFile = {
      mimetype: mime,
      key: jwk,
      iv: Buffer.from(iv).toString("base64").replace(/=/g, ""),
      hashes: { sha256: Buffer.from(hash).toString("base64").replace(/=/g, "") },
      v: "v2",
    };
    return [encryptedBytes, encryptedFile];
  }

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

          var data = new Uint8Array(fileContents);
          let thumbnailData = undefined;
          let thumbnailInfo = undefined;

          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 (thumbnail) {
            thumbnailData = thumbnail.data;
            thumbnailInfo = {
              mimetype: thumbnail.mimetype,
              size: thumbnail.size,
              w: thumbnail.w,
              h: thumbnail.h,
            };
          }

          if (file.type.startsWith("image/")) {
            msgtype = "m.image";

            // Generate thumbnail?
            if (dimensions && !thumbnail) {
              const w = dimensions.width;
              const h = dimensions.height;
              if (w > THUMBNAIL_MAX_WIDTH || h > THUMBNAIL_MAX_HEIGHT) {
                var aspect = w / h;
                var newWidth = parseInt((w > h ? THUMBNAIL_MAX_WIDTH : THUMBNAIL_MAX_HEIGHT * aspect).toFixed());
                var newHeight = parseInt((w > h ? THUMBNAIL_MAX_WIDTH / aspect : THUMBNAIL_MAX_HEIGHT).toFixed());
                const scaled = await imageResize(file, {
                  format: "webp",
                  width: newWidth,
                  height: newHeight,
                  outputType: "blob",
                }).catch(() => {
                  return Promise.resolve(undefined);
                });
                if (scaled && file.size > scaled.size) {
                  thumbnailData = new Uint8Array(await scaled.arrayBuffer());
                  thumbnailInfo = {
                    mimetype: scaled.type,
                    size: scaled.size,
                    h: newHeight,
                    w: newWidth,
                  };
                }
              }
            }
          } else if (file.type.startsWith("audio/")) {
            msgtype = "m.audio";
          } else if (file.type.startsWith("video/")) {
            msgtype = "m.video";
          }

          let messageContent = {
            body: description,
            info: info,
            msgtype: msgtype,
          };

          if (mediaMetadata) {
            const interventionFlags = mediaMetadataToMediaInterventionFlags(mediaMetadata);
            messageContent[CLIENT_EVENT_MEDIA_INTERVENTION_FLAGS] = JSON.stringify(interventionFlags);
          }

          // 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;
          }

          let totalBytes = 0;
          let thumbBytes = 0;

          const useEncryption = matrixClient.isRoomEncrypted(roomId);

          const dataUploadOpts = {
            type: useEncryption ? "application/octet-stream" : file.type,
            name: description,
            progressHandler: ({ loaded, total }) => {
              if (onUploadProgress) {
                onUploadProgress({ loaded: loaded + thumbBytes, total: totalBytes });
              }
            },
            onlyContentUri: false,
          };

          if (useEncryption) {
            const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(data, file.type);
            messageContent.file = encryptedFile;
            data = encryptedBytes;
          }

          totalBytes = data.length;

          if (thumbnailData) {
            messageContent.thumbnail_info = thumbnailInfo;
            if (useEncryption) {
              const [encryptedBytes, encryptedFile] = await this.encryptFileAndGenerateInfo(thumbnailData, file.type);
              messageContent.info.thumbnail_file = encryptedFile;
              thumbnailData = encryptedBytes;
            }
            thumbBytes = thumbnailData.length;
            totalBytes += thumbnailData.length;
            const thumbnailUploadOpts = {
              type: useEncryption ? "application/octet-stream" : file.type,
              name: "thumb:" + description,
              progressHandler: ({ loaded, total }) => {
                if (onUploadProgress) {
                  onUploadProgress({ loaded: loaded, total: totalBytes });
                }
              },
              onlyContentUri: false,
            };
            const thumbUploadPromise = matrixClient.uploadContent(thumbnailData, thumbnailUploadOpts);
            uploadPromise.onAbort = () => {
              matrixClient.cancelUpload(thumbUploadPromise);
            };
            const thumbnailResponse = await thumbUploadPromise;
            if (useEncryption) {
              messageContent.info.thumbnail_file.url = thumbnailResponse.content_uri;
            } else {
              messageContent.info.thumbnail_url = thumbnailResponse.content_uri;
            }
          }

          const dataUploadPromise = matrixClient.uploadContent(data, dataUploadOpts);
          uploadPromise.onAbort = () => {
            matrixClient.cancelUpload(dataUploadPromise);
          };
          const response = await dataUploadPromise;
          if (useEncryption) {
            messageContent.file.url = response.content_uri;
          } else {
            messageContent.url = response.content_uri;
          }

          // Generate audio waveforms
          if (msgtype == "m.audio") {
            await this.generateWaveform(fileContents, messageContent);
          }

          const result = await this.sendMessage(matrixClient, roomId, "m.room.message", messageContent);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      reader.onerror = (err) => {
        reject(err);
      };
      reader.readAsArrayBuffer(file);
    });
    return uploadPromise;
  }

  async generateWaveform(data, messageContent) {
    if (!(window.AudioContext || window.webkitAudioContext)) {
      return; // No support
    }
    try {
      const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      if (audioCtx) {
        const audioBuffer = await audioCtx.decodeAudioData(data);
        if (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;

          // if duration is not set, do that here, since we have it
          if (!messageContent.info.duration) {
            messageContent.info.duration = parseInt((1000 * audioBuffer.duration).toFixed());
          }
        }
      }
    } catch (error) {
      return;
    }
  }

  /**
   * 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"]) {
        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() {
    var images = [];
    const modules = import.meta.glob("@/assets/avatars/*.{jpeg,jpg,png}", {
      query: "?url",
      import: "default",
      eager: true,
    });
    Object.keys(modules).map((path) => {
      var name = path.split("_")[1];
      name = name.slice(0, name.indexOf("."));
      name = name.charAt(0).toUpperCase() + name.slice(1);
      const image = modules[path];
      const randomNumber = parseInt(this.randomString(4, "0123456789")).toFixed();
      const title = "Guest " + name + " " + randomNumber;
      images.push({ id: path, image: image, name: title, title: title });
    });
    return images;
  }

  /**
   * For one of the predefined avatars, make it system unique by adding a random PNG comment header.
   */
  makeUniqueAvatar(image) {
    var list = png.splitChunk(image);
    
    // append
    var iend = list.pop(); // remove IEND
    var newchunk = png.createChunk("tEXt","Comment\0Keanu avatar - " + this.randomPass());
    list.push(newchunk);
    list.push(iend);

    // join
    var newpng = png.joinChunk(list);
    return newpng;
  }

  setAvatar(matrix, file, onUploadProgress, makeUnique) {
    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,
          };
          let data = response.data;
          
          // If making a system unique avatar, we add a random PNG header in here
          if (makeUnique) {
            data = this.makeUniqueAvatar(data);
          }

          var avatarUri;
          matrix.matrixClient
            .uploadContent(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(matrixClient.mxcUrlToHttp(messageContent.url, 80, 80, "scale", undefined, undefined, true));
            //            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;

            const buffer = Uint8Array.from(window.atob(e.target.result.replace(/^data[^,]+,/, "")), (v) =>
              v.charCodeAt(0)
            );
            var dimens = imageSize(buffer);

            // 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());
              imageResize(image, {
                format: "png",
                width: newWidth,
                height: newHeight,
                outputType: "blob",
              })
                .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");
  }

  parseExifDate(exifDateString) {
    // Use a regular expression to match and capture the date/time components.
    const regex = /(\d{4}):(\d{2}):(\d{2})\s(\d{2}):(\d{2}):(\d{2})/;
    const match = exifDateString.match(regex);

    if (match) {
      // Extract components and convert to numbers. Note that months are 0-indexed.
      const year = parseInt(match[1], 10);
      const month = parseInt(match[2], 10) - 1; 
      const day = parseInt(match[3], 10);
      const hour = parseInt(match[4], 10);
      const minute = parseInt(match[5], 10);
      const second = parseInt(match[6], 10);
      return new Date(year, month, day, hour, minute, second);
    }
    return undefined;
  }

  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, useAuthedMedia, event) {
    console.log("DOWNLOAD");
    this.getAttachment(matrixClient, useAuthedMedia, 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) {
    const size = this.getFileSize(event);
    const res = prettyBytes(size);
    return res;
  }

  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);
  }

  /**
   * Possibly convert numerals to local representation (currently only for "bo" locale)
   * @param str String in which to convert numerals [0-9]
   * @returns converted string
   */
  toLocalNumbers = (str) => {
    if (i18n.locale == "my") {
      // Translate to burmese numerals
      var result = "";
      for (var i = 0; i < str.length; i++) {
        var c = str.charCodeAt(i);
        if (c >= 48 && c <= 57) {
          result += String.fromCharCode(c + 0x1040 - 48);
        } else {
          result += String.fromCharCode(c);
        }
      }
      return result;
    } else if (i18n.locale == "bo") {
      // Translate to tibetan numerals
      result = "";
      for (i = 0; i < str.length; i++) {
        c = str.charCodeAt(i);
        if (c >= 48 && c <= 57) {
          result += String.fromCharCode(c + 0x0f20 - 48);
        } else {
          result += String.fromCharCode(c);
        }
      }
      return result;
    }

    return str;
  };

  deepToRaw(sourceObj) {
    const objectIterator = (input) => {
      if (Array.isArray(input)) {
        return input.map((item) => objectIterator(item));
      }
      if (isRef(input) || isReactive(input) || isProxy(input)) {
        return objectIterator(toRaw(input));
      }
      if (input && typeof input === "object") {
        return Object.keys(input).reduce((acc, key) => {
          acc[key] = objectIterator(input[key]);
          return acc;
        }, {});
      }
      return input;
    };
    return objectIterator(sourceObj);
  }
}
export default new Util();
