import olm from "@matrix-org/olm/olm";
global.Olm = olm;
import * as sdk from "matrix-js-sdk";
import { TimelineWindow, EventTimeline, EventStatus } from "matrix-js-sdk";
import util, { STATE_EVENT_ROOM_DELETED, STATE_EVENT_ROOM_TYPE, ROOM_TYPE_CHANNEL, ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_DEFAULT } from "../plugins/utils";
import User from "../models/user";

const LocalStorageCryptoStore =
  require("matrix-js-sdk/lib/crypto/store/localStorage-crypto-store").LocalStorageCryptoStore;

export const CHANNEL_POWER_LEVELS = {
  "m.room.encrypted": 0, // NOTE! Since practically all events in encrypted rooms get sent as "m.room.encrypted" we need to set
  // power to 0 here. Otherwise we would not be able to send quick reactions or poll responses...
  "m.poll.response": 0,
  "org.matrix.msc3381.poll.response": 0,
  "m.reaction": 0,
  "m.room.redaction": 0,
};

export default {
  install(Vue, options) {
    if (!options || !options.store) {
      throw new Error("Please initialise plugin with a Vuex store.");
    }

    // Set User-Agent headers.
    // Update: browser do not allow this, "Refused to set unsafe header "User-Agent""
    // Keep this code around however, since it's an example of how to add headers to a request...
    // sdk.wrapRequest((orig, opts, callback) => {
    //     opts.headers = opts.headers || {}
    //     opts.headers['User-Agent'] = "Keanu";
    //     var ret = orig(opts, callback);
    //     return ret;
    // });
    const store = options.store;
    const i18n = options.i18n;

    const matrixService = new Vue({
      store,
      i18n,
      data() {
        return {
          matrixClient: null,
          matrixClientReady: false,
          rooms: [],
          userDisplayName: null,
          userAvatar: null,
          currentRoom: null,
          userCanSendMessageInCurrentRoom: true,
          userCanSendReactionAndAnswerPollInCurrentRoom: true,
          currentRoomBeingPurged: false,
          notificationCount: 0,
        };
      },
      mounted() {
        console.log("Matrix service mounted");
      },

      computed: {
        ready() {
          return this.matrixClient != null && this.matrixClientReady;
        },

        currentUser() {
          return this.$store.state.auth.user;
        },

        currentUserId() {
          const user = this.currentUser || {};
          return user.user_id;
        },

        currentUserDisplayName() {
          if (this.ready) {
            const user = this.matrixClient.getUser(this.currentUserId) || {};
            return this.userDisplayName || user.displayName;
          }
          return null;
        },

        currentUserMXDomain() {
          return User.domainPart(this.currentUserId) || this.$config.defaultMatrixDomainPart;
        },

        currentRoomId() {
          return this.$store.state.currentRoomId;
        },

        joinedRooms() {
          return this.rooms.filter((room) => {
            return room.selfMembership === "join";
          });
        },

        invites() {
          return this.rooms.filter((room) => {
            return room.selfMembership === "invite";
          });
        },

        joinedAndInvitedRooms() {
          return this.rooms.filter((room) => {
            return room.selfMembership === "join" || room.selfMembership === "invite";
          });
        },
      },

      watch: {
        currentRoomId: {
          immediate: true,
          handler(roomId) {
            this.currentRoom = this.getRoom(roomId);
          },
        },
        currentRoom: {
          immediate: true,
          handler(room) {
            if (room) {
              this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(room.roomId, this.currentUserId);
              this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(room.roomId, this.currentUserId);
            } else {
              this.userCanSendMessageInCurrentRoom = true;
              this.userCanSendReactionAndAnswerPollInCurrentRoom = true;
            }
          },
        },
      },

      methods: {
        createCryptoStore() {
          console.log("create crypto store");
          return new LocalStorageCryptoStore(this.$store.getters.storage);
        },
        login(user, registrationFlowHandler, createUser = false) {
          return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
            const tempMatrixClient = sdk.createClient({
              baseUrl: baseUrl,
              idBaseUrl: this.$config.identityServer,
            });
            var promiseLogin;

            const self = this;
            if (user.access_token) {
              // Logged in on "real" account
              promiseLogin = Promise.resolve(user);
            } else if (createUser || (user.is_guest && (!user.user_id || user.registration_session))) {
              // Generate random username and password. We don't user REAL matrix
              // guest accounts because 1. They are not allowed to post media, 2. They
              // can not use avatars and 3. They can not seamlessly be upgraded to real accounts.
              //
              // Instead, we use an ILAG approach, Improved Landing as Guest.
              const userId =
                createUser || user.registration_session ? user.user_id : util.randomUser(this.$config.userIdPrefix);
              const pass = createUser || user.registration_session ? user.password : util.randomPass();

              const extractAndSaveUser = (response) => {
                var u = Object.assign({}, response);
                u.password = pass;
                u.is_guest = true;
                this.$store.commit("setUser", u);
                return u;
              };

              promiseLogin = tempMatrixClient
                .register(userId, pass, user.registration_session || null, {
                  type: "m.login.dummy",
                  initial_device_display_name: this.$config.appName,
                })
                .then((response) => {
                  return extractAndSaveUser(response);
                })
                .catch((error) => {
                  if (registrationFlowHandler && error.httpStatus == 401 && error.data) {
                    const registrationSession = error.data.session;

                    // Store user, pass and session, so we can resume if network failure occurs etc.
                    //
                    var u = {};
                    u.user_id = userId;
                    u.password = pass;
                    u.is_guest = true;
                    u.registration_session = registrationSession;
                    this.$store.commit("setUser", u);

                    return registrationFlowHandler(tempMatrixClient, error.data).then((response) =>
                      extractAndSaveUser(response)
                    );
                  } else {
                    console.error(error);
                  }
                  throw error;
                });
            } else {
              var data = {
                user: User.localPart(user.user_id),
                password: user.password,
                type: "m.login.password",
                initial_device_display_name: this.$config.appName,
              };
              if (user.device_id) {
                data.device_id = user.device_id;
              }
              promiseLogin = tempMatrixClient.login("m.login.password", data).then((response) => {
                var u = Object.assign({}, response);
                if (user.is_guest) {
                  // Copy over needed properties
                  u = Object.assign(user, response);
                }
                this.$store.commit("setUser", u);
                return u;
              });
            }

            return promiseLogin.then((user) => {
              return self.getMatrixClient(user);
            });
          });
        },

        clearCryptoStore() {
          // Clear crypto related data
          // TODO - for some reason "clearStores" called in "logout" only clears the "account" crypto
          // data item, not all sessions etc. Why? We need to do that manually here!
          const toRemove = [];
          const storage = this.$store.getters.storage;
          for (let i = 0; i < storage.length; ++i) {
            const key = storage.key(i);
            if (key.startsWith("crypto.")) toRemove.push(key);
          }
          for (const key of toRemove) {
            storage.removeItem(key);
          }
        },

        logout() {
          if (this.matrixClient) {
            this.removeMatrixClientListeners(this.matrixClient);
            this.matrixClient.stopClient();
            this.matrixClient.clearStores().then(() => {
              this.clearCryptoStore();
            });
            this.matrixClient = null;
            this.matrixClientReady = false;
          } else {
            this.clearCryptoStore();
          }

          this.$store.commit("setUser", null);
          this.$store.commit("setCurrentRoomId", null);
          this.rooms = [];
          this.userDisplayName = null;
          this.userAvatar = null;
          this.currentRoom = null;
          this.notificationCount = 0;
        },

        initClient() {
          this.reloadRooms();
          this.matrixClientReady = true;
          this.matrixClient.emit("Matrix.initialized", this.matrixClient);
          this.matrixClient
            .getProfileInfo(this.currentUserId)
            .then((info) => {
              console.log("Got user profile: " + JSON.stringify(info));
              this.userDisplayName = info.displayname;
              this.userAvatar = info.avatar_url;
            })
            .catch((err) => {
              console.log("Failed to get user profile: ", err);
            });
        },

        getMatrixClient(user) {
          if (user === undefined) {
            user = this.$store.state.auth.user;
          }
          if (this.matrixClientReady) {
            return new Promise((resolve, ignoredreject) => {
              resolve(user);
            });
          } else if (this.matrixClient) {
            return new Promise((resolve, ignoredreject) => {
              this.matrixClient.once("Matrix.initialized", (ignoredclient) => {
                resolve(user);
              });
            });
          }

          const matrixStore = new sdk.MemoryStore(this.$store.getters.storage);

          return util.getMatrixBaseUrl(user, this.$config).then((baseUrl) => {
            var opts = {
              baseUrl: baseUrl,
              userId: user.user_id,
              store: matrixStore,
              deviceId: user.device_id,
              accessToken: user.access_token,
              timelineSupport: true,
              unstableClientRelationAggregation: true,
              //useAuthorizationHeader: true
            };
            this.matrixClient = sdk.createClient(opts);
            // if (user.is_guest) {
            //     this.matrixClient.setGuest(true);
            // }
            return this.matrixClient
              .initCrypto()
              .then(() => {
                console.log("Crypto initialized");

                this.addMatrixClientListeners(this.matrixClient);

                this.matrixClient.startClient();
                return this.matrixClient;
              })
              .then((matrixClient) => {
                if (matrixClient.isInitialSyncComplete()) {
                  console.log("Initial sync done already!");
                  return matrixClient;
                } else {
                  return new Promise((resolve, reject) => {
                    matrixClient.once("sync", function (state, ignoredprevState, ignoredres) {
                      console.log(state); // state will be 'PREPARED' when the client is ready to use
                      if (state == "PREPARED") {
                        resolve(matrixClient);
                      } else if (state == "ERROR") {
                        reject("Error syncing");
                      }
                    });
                  });
                }
              })
              .then(() => {
                // Ready to use! Start by loading rooms.
                this.initClient();
                return user;
              });
          });
        },

        /**
         * Returns a promise that will log us into the Matrix.
         *
         * Will use a real account, if we have one, otherwise will create
         * a random account.
         */
        getLoginPromise(registrationFlowHandler) {
          if (this.ready) {
            return Promise.resolve(this.currentUser);
          }
          return this.$store.dispatch("login", {
            user: this.currentUser || new User("", "", true),
            registrationFlowHandler,
          });
        },

        addMatrixClientListeners(client) {
          if (client) {
            client.setMaxListeners(100); // Increate max number of listeners.
            client.on("event", this.onEvent);
            client.on("Room", this.onRoom);
            client.on("Session.logged_out", this.onSessionLoggedOut);
            client.on("Room.myMembership", this.onRoomMyMembership);
          }
        },

        removeMatrixClientListeners(client) {
          if (client) {
            client.off("event", this.onEvent);
            client.off("Room", this.onRoom);
            client.off("Session.logged_out", this.onSessionLoggedOut);
            client.off("Room.myMembership", this.onRoomMyMembership);
          }
        },

        onEvent(event) {
          switch (event.getType()) {
            case "m.room.topic":
              {
                const room = this.matrixClient.getRoom(event.getRoomId());
                if (room) {
                  Vue.set(room, "topic", event.getContent().topic);
                }
              }
              break;

            case "m.room.avatar":
              {
                const room = this.matrixClient.getRoom(event.getRoomId());
                if (room) {
                  Vue.set(
                    room,
                    "avatar",
                    room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true)
                  );
                }
              }
              break;

            case "m.room.power_levels":
              {
                if (this.currentRoom && event.getRoomId() == this.currentRoom.roomId) {
                  this.userCanSendMessageInCurrentRoom = this.userCanSendMessageInRoom(event.getRoomId(), this.currentUserId);
                  this.userCanSendReactionAndAnswerPollInCurrentRoom = this.userCanSendReactionAndAnswerPollInRoom(event.getRoomId(), this.currentUserId);
                }
              }
              break;

            case "m.room.join_rules":
              {
                const room = this.matrixClient.getRoom(event.getRoomId());
                if (room && room.getJoinRule() == "private" && room.selfMembership == "invite") {
                  // We have an invite to a room that's now "private"? This is most probably a deleted DM room.
                  // Reject the invite, i.e. call "leave" on it.
                  this.matrixClient.leave(room.roomId);
                }
              }
              break;

            case STATE_EVENT_ROOM_DELETED:
              {
                const room = this.matrixClient.getRoom(event.getRoomId());
                if (room && room.currentState) {
                  // Before we do anything, make sure the sender is an admin!
                  // Also, do not react if WE are the sender, since we are probably
                  // busy doing the rest of the purging process...
                  if (room.currentState.maySendStateEvent("m.room.power_levels", event.getSender())) {
                    if (event.getSender() !== this.currentUserId) {
                      this.leaveRoomAndNavigate(room.roomId).then(() => {
                        this.matrixClient.forget(room.roomId, true);
                      });
                    }
                  }
                }
              }
              break;
          }
          this.updateNotificationCount();
        },

        onRoom(room) {
          if (room.selfMembership === "invite") {
            this.matrixClient
              .getRoomTags(room.roomId)
              .then((reply) => {
                if (Object.keys(reply.tags).includes("m.server_notice")) {
                  Vue.set(room, "isServiceNoticeRoom", true);
                }
              })
              .catch((error) => {
                console.error(error);
              });
          }
          this.reloadRooms();
          this.updateNotificationCount();
        },

        onRoomMyMembership(room) {
          if (room.selfMembership === "invite") {
            // Invitation. Need to call "recalculate" to pick
            // up room name, not sure why exactly.
            room.recalculate();
          }
          this.reloadRooms();
        },

        onSessionLoggedOut() {
          console.log("Logged out!");
          if (this.matrixClient) {
            this.removeMatrixClientListeners(this.matrixClient);
            this.matrixClient.stopClient();
            this.matrixClient = null;
            this.matrixClientReady = false;
          }

          // For "real" accounts we totally wipe the user object, but for "guest"
          // accounts (i.e. created from random data and with password never changed)
          // we need to hang on to the generated password and use that to login to a new
          // session, so only wipe the token in s that case.
          // Clear the access token
          var user = this.$store.state.auth.user;
          if (user.is_guest) {
            delete user.access_token;
            this.$store.commit("setUser", user);

            // Login again
            this.login(user).catch((error) => {
              if (error.data.errcode === "M_FORBIDDEN" && this.currentUser.is_guest) {
                // Guest account and password don't work. We are in a strange state, probably because
                // of server cleanup of accounts or similar. Wipe account and restart...
                this.$store.commit("setUser", null);
              }
              this.$store.commit("setCurrentRoomId", null);
              this.$navigation.push({ path: "/login" }, -1);
            });
          } else {
            this.$store.commit("setUser", null);
            this.$store.commit("setCurrentRoomId", null);
            this.$navigation.push({ path: "/login" }, -1);
          }
        },

        reloadRooms() {
          // TODO - do incremental update instead of replacing the whole array
          // each time!
          var updatedRooms = this.matrixClient.getVisibleRooms();
          updatedRooms = updatedRooms.filter((room) => {
            return room.selfMembership && (room.selfMembership == "invite" || room.selfMembership == "join") && room.currentState.getStateEvents(STATE_EVENT_ROOM_DELETED).length == 0;
          });
          updatedRooms.forEach((room) => {
            if (!room.avatar) {
              Vue.set(room, "avatar", room.getAvatarUrl(this.matrixClient.getHomeserverUrl(), 80, 80, "scale", true));
            }
          });
          Vue.set(this, "rooms", updatedRooms);

          const resolvedId =
            this.currentRoomId && this.currentRoomId.startsWith("#")
              ? this.matrixClient.resolveRoomAlias(this.currentRoomId).then((r) => r.room_id)
              : Promise.resolve(this.currentRoomId);
          resolvedId
            .then((roomId) => {
              const currentRoom = this.getRoom(roomId);
              if (this.currentRoom != currentRoom) {
                this.currentRoom = currentRoom;
              }
            })
            .catch((ignorederror) => {});
        },

        setCurrentRoomId(roomId) {
          this.$store.commit("setCurrentRoomId", roomId);
          this.currentRoom = this.getRoom(roomId);
        },

        getRoom(roomId) {
          if (!roomId) {
            return null;
          }
          var room = null;
          if (this.matrixClient) {
            const visibleRooms = this.matrixClient.getRooms();
            room = visibleRooms.find((room) => {
              if (roomId.startsWith("#")) {
                return room.getCanonicalAlias() == roomId;
              }
              return room.roomId == roomId;
            });
          }
          return room || null;
        },

        /**
         * Return all users we are in a "invite" only room with!
         */
        getAllFriends() {
          var ids = {};
          const ret = [];
          for (const room of this.rooms) {
            if (room.selfMembership == "join" && this.getRoomJoinRule(room) == "invite") {
              for (const member of room.getJoinedMembers()) {
                if (member.userId != this.currentUserId && !ids[member.userId]) {
                  ids[member.userId] = member;
                  ret.push(member);
                }
              }
            }
          }
          ret.sort((a, b) => {
            const aName = a.user ? a.user.displayName : a.name;
            const bName = b.user ? b.user.displayName : b.name;
            return aName.localeCompare(bName);
          });
          return ret;
        },

        getRoomJoinRule(room) {
          if (room) {
            const joinRules = room.currentState.getStateEvents("m.room.join_rules", "");
            return joinRules && joinRules.getContent().join_rule;
          }
          return null;
        },

        getRoomHistoryVisibility(room) {
          if (room) {
            const historyVisibility = room.currentState.getStateEvents("m.room.history_visibility", "");
            return historyVisibility && historyVisibility.getContent().history_visibility;
          }
          return null;
        },

        leaveRoom(roomId) {
          return this.matrixClient.leave(roomId).then(() => {
            this.$store.commit("setCurrentRoomId", null);
            this.rooms = this.rooms.filter((room) => {
              room.roomId != roomId;
            });
            //this.matrixClient.store.removeRoom(roomId);
            //this.matrixClient.forget(roomId, true, undefined);
          });
        },

        /**
         * Leave the room, and if this is the last room we are in, navigate to the "goodbye" page.
         * Otherwise, navigate to home.
         * @param roomId
         */
        leaveRoomAndNavigate(roomId) {
          const joinedRooms = this.joinedRooms;
          const isLastRoomWeAreJoinedTo = joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == roomId;
          return this.leaveRoom(roomId).then(() => {
            if (isLastRoomWeAreJoinedTo) {
              this.$navigation.push({ name: "Goodbye" }, -1);
            } else {
              this.$navigation.push({ name: "Home", params: { roomId: null } }, -1);
            }
          });
        },

        kickUser(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            this.matrixClient.kick(roomId, userId, "");
          }
        },

        banUser(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            this.matrixClient.ban(roomId, userId, "");
          }
        },

        makeAdmin(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
              if (powerLevelEvent) {
                this.matrixClient.setPowerLevel(roomId, userId, 100, powerLevelEvent);
              }
            }
          }
        },

        makeModerator(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            const room = this.getRoom(roomId);
            console.log("Room", room);
            if (room && room.currentState) {
              const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
              if (powerLevelEvent) {
                this.matrixClient.setPowerLevel(roomId, userId, 50, powerLevelEvent);
              }
            }
          }
        },

        revokeModerator(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
              if (powerLevelEvent) {
                this.matrixClient.setPowerLevel(roomId, userId, 0, powerLevelEvent);
              }
            }
          }
        },

        /**
         * Returns true if the current user is joined to the given room.
         * @param roomIdOrAlias
         * @returns Promise<Bool> - Whether the user is joined to the room or not
         */
        isJoinedToRoom(roomIdOrAlias) {
          if (roomIdOrAlias && this.matrixClient) {
            try {
              const resolvedRoomId = roomIdOrAlias.startsWith("#")
                ? this.matrixClient.resolveRoomAlias(roomIdOrAlias).then((res) => res.room_id)
                : Promise.resolve(roomIdOrAlias);
              return resolvedRoomId.then((roomId) => {
                return this.matrixClient.getJoinedRooms().then((rooms) => {
                  return rooms.joined_rooms.includes(roomId);
                });
              });
            } catch (ignorederror) {
              console.error(ignorederror);
              return Promise.resolve(false);
            }
          }
          return Promise.resolve(false);
        },

        isReadOnlyRoom(roomId) {
          if (this.matrixClient && roomId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
              if (powerLevelEvent) {
                if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) {
                  return Object.keys(powerLevelEvent.getContent().events).length == 0;
                }
                return powerLevelEvent.getContent().events_default > 0;
              }
            }
          }
          return false;
        },

        setReadOnlyRoom(roomId, readOnly) {
          if (this.matrixClient && roomId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
              if (powerLevelEvent) {
                let content = powerLevelEvent.getContent();
                if (this.roomType(roomId) == ROOM_TYPE_CHANNEL) {
                  content.events = readOnly ? {} : CHANNEL_POWER_LEVELS
                } else {
                  content.events_default = readOnly ? 50 : 0;
                }
                this.matrixClient.sendStateEvent(room.roomId, "m.room.power_levels", content);
              }
            }
          }
        },

        userCanSendMessageInRoom(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId);
              return isAdmin || (this.roomType(roomId) != ROOM_TYPE_CHANNEL && !this.isReadOnlyRoom(roomId));
            }
          }
          return true;
        },

        userCanSendReactionAndAnswerPollInRoom(roomId, userId) {
          if (this.matrixClient && roomId && userId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              let isAdmin = room.currentState.maySendEvent("m.room.power_levels", this.currentUserId);
              return isAdmin || !this.isReadOnlyRoom(roomId);
            }
          }
          return true;          
        },

        roomType(roomId) {
          if (this.matrixClient && roomId) {
            const room = this.getRoom(roomId);
            if (room && room.currentState) {
              const roomTypeEvent = room.currentState.getStateEvents(STATE_EVENT_ROOM_TYPE, "") || room.currentState.getStateEvents("m.room.create", "");
              if (roomTypeEvent) {
                const roomType = roomTypeEvent.getContent().type;
                if ([ROOM_TYPE_FILE_MODE, ROOM_TYPE_VOICE_MODE, ROOM_TYPE_CHANNEL].includes(roomType)) {
                  return roomType;
                }
              }
            }
          }
          return ROOM_TYPE_DEFAULT;
        },

        /**
         * Purge the room with the given id! This means:
         * - Make room invite only
         * - Disallow guest access
         * - Set history visibility to 'joined'
         * - Redact all events
         * - Kick all members
         * @param roomId
         */
        purgeRoom(roomId, statusCallback) {
          this.currentRoomBeingPurged = true;

          //console.log("Purge room");

          const sleep = (ms) => {
            return new Promise((resolve) => setTimeout(resolve, ms));
          };

          const withRetry = (codeBlock) => {
            return codeBlock().catch((error) => {
              if (error && error.errcode == "M_LIMIT_EXCEEDED") {
                var retry = 1000;
                if (error.data) {
                  const retryIn = error.data.retry_after_ms;
                  retry = Math.max(retry, retryIn ? retryIn : 0);
                }
                console.log("Rate limited, retry in", retry);
                return sleep(retry).then(() => withRetry(codeBlock));
              } else {
                return Promise.resolve();
              }
            });
          };

          const oldGlobalErrorSetting = this.matrixClient.getGlobalErrorOnUnknownDevices();
          return new Promise((resolve, reject) => {
            const room = this.getRoom(roomId);
            if (!room) {
              reject("Room not found!");
              return;
            }

            // Remove any possible pending events
            room.getLiveTimeline().getEvents().filter((e) => [EventStatus.ENCRYPTING, sdk.EventStatus.QUEUED].includes(e.status)).forEach((e) => {
              //console.log("Cancel pending event!");
              this.matrixClient.cancelPendingEvent(e);
            });

            const timelineWindow = new TimelineWindow(this.matrixClient, room.getUnfilteredTimelineSet(), {});
            const self = this;

            //console.log("Purge: set invite only");
            statusCallback(this.$t("room.purge_set_room_state"));
            withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.join_rules", { join_rule: "private" }, ""))
              .then(() => {
                //console.log("Purge: forbid guest access");
                return withRetry(() => this.matrixClient.sendStateEvent(
                  roomId,
                  "m.room.guest_access",
                  { guest_access: "forbidden" },
                  ""
                ));
              })
              .then(() => {
                //console.log("Purge: set history visibility to 'joined'");
                return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.history_visibility", {
                  history_visibility: "joined",
                }));
              })
              .then(() => {
                return withRetry(() => this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" }));
              })
              .then(() => {
                // Set message retention 1 minute (may be limited by server)
                return withRetry(() => this.matrixClient.sendStateEvent(roomId, "m.room.retention", { max_lifetime: 60000 }));
              })
              .then(() => {
                //console.log("Purge: create timeline");
                return timelineWindow.load(null, 100);
              })
              .then(() => {
                const getMoreIfAvailable = function _getMoreIfAvailable() {
                  if (timelineWindow.canPaginate(EventTimeline.BACKWARDS)) {
                    //console.log("Purge: page back");
                    return timelineWindow.paginate(EventTimeline.BACKWARDS, 100, true, 5).then((gotmore) => {
                      if (gotmore) {
                        return _getMoreIfAvailable.call(self);
                      }
                      return Promise.resolve("Done");
                    });
                  } else {
                    return Promise.resolve("Done");
                  }
                }.bind(self);
                return getMoreIfAvailable();
              })
              .then(() => {
                //console.log("Purge: redact events");
                statusCallback(this.$t("room.purge_redacting_events"));
                // First ignore unknown device errors
                this.matrixClient.setGlobalErrorOnUnknownDevices(false);
                const allEvents = timelineWindow.getEvents().filter((event) => {
                  return (
                    !event.isRedacted() &&
                    !event.isRedaction() &&
                    !event.isState() &&
                    (!room.currentState || room.currentState.maySendRedactionForEvent(event, this.currentUserId))
                  );
                });

                const redactFirstEvent = (events) => {
                  statusCallback(
                    this.$t("room.purge_redacting_events", {
                      count: allEvents.length - events.length + 1,
                      total: allEvents.length,
                    })
                  );
                  if (events.length == 0) {
                    return Promise.resolve(true);
                  }
                  const event = events[0];
                  return withRetry(() => this.matrixClient.redactEvent(event.getRoomId(), event.getId())).then(() =>
                    redactFirstEvent(events.slice(1))
                  );
                };

                return redactFirstEvent(allEvents);
              })
              .then(() => {
                //console.log("Purge: kick members");
                statusCallback(this.$t("room.purge_removing_members"));
                var joined = room.getMembersWithMembership("join");
                var invited = room.getMembersWithMembership("invite");
                var allMembers = joined.concat(invited);

                const me = allMembers.find((m) => m.userId == self.currentUserId);

                const kickFirstMember = (members) => {
                  //console.log(`Kicking ${members.length} members`);
                  statusCallback(
                    this.$t("room.purge_removing_members", {
                      count: allMembers.length - members.length + 1,
                      total: allMembers.length,
                    })
                  );
                  if (members.length == 0) {
                    return Promise.resolve(true);
                  }
                  const member = members[0];
                  if (member.userId == self.currentUserId) {
                    return kickFirstMember(members.slice(1));
                  } else {
                    // Slight pause to avoid rate limiting.
                    return sleep(0.1)
                      .then(() => withRetry(() => {
                        if (member.membership == "invite" && me && me.powerLevel <= member.powerLevel) {
                          // The user is invited, but we can't kick them because of power levels.
                          // Send a new invite with reason set to "Room Deleted".
                          // The client will be sent stripped room state, and can from that see the
                          // join_rule of "private". It will then "leave", i.e. reject the invite.
                          return this.matrixClient.invite(roomId, member.userId, "Room Deleted");
                        }
                        return this.matrixClient.kick(roomId, member.userId, "Room Deleted")
                      }))
                      .then(() => kickFirstMember(members.slice(1)));
                  }
                };

                return kickFirstMember(allMembers);
              })
              .then(() => {
                return withRetry(() =>
                  this.matrixClient.sendStateEvent(roomId, STATE_EVENT_ROOM_DELETED, { status: "deleted" })
                );
              })
              .then(() => {
                statusCallback(null);
                this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
                return withRetry(() => this.leaveRoom(roomId));
              })
              .then(() => {
                this.currentRoomBeingPurged = false;
                resolve(true); // Done!
              })
              .catch((err) => {
                console.error("Error purging room", err);
                this.currentRoomBeingPurged = false;
                this.matrixClient.setGlobalErrorOnUnknownDevices(oldGlobalErrorSetting);
                reject(err);
              });
          });
        },

        /**
         * Get a private chat room with the given user. Searches through our rooms to see
         * if a suitable room already exists. If not, one is created.
         * @param {*} userId The user to chat with.
         */
        getOrCreatePrivateChat(userId) {
          return new Promise((resolve, reject) => {
            for (const room of this.rooms) {
              // Is the other member the one we are looking for?
              if (this.isDirectRoomWith(room, userId)) {
                if (room.getMyMembership() == "invite") {
                  this.matrixClient.joinRoom(room.roomId);
                } else {
                  var member = room.getMember(userId);
                  if (member && member.membership != "join") {
                    // Resend invite
                    this.matrixClient.invite(room.roomId, userId);
                  }
                }
                resolve(room);
                return;
              }
            }

            // No room found, create one
            //
            const createRoomOptions = {
              visibility: "private", // Not listed!
              preset: "private_chat",
              is_direct: true,
              initial_state: [
                {
                  type: "m.room.encryption",
                  state_key: "",
                  content: {
                    algorithm: "m.megolm.v1.aes-sha2",
                  },
                },
                {
                  type: "m.room.guest_access",
                  state_key: "",
                  content: {
                    guest_access: "forbidden",
                  },
                },
                {
                  type: "m.room.history_visibility",
                  state_key: "",
                  content: {
                    history_visibility: "invited",
                  },
                },
                {
                  type: "m.room.power_levels",
                  state_key: "",
                  content: {
                    users: {
                      [this.currentUserId]: 100,
                      [userId]: 100,
                    },
                  },
                },
              ],
              invite: [userId],
            };
            return this.matrixClient
              .createRoom(createRoomOptions)
              .then(({ room_id, room_alias }) => {
                resolve(this.getRoom(room_alias || room_id));
              })
              .catch((error) => {
                reject(error);
              });
          });
        },

        /**
         * Return true if this room is a direct room with the given user.
         * @param { } room
         * @param {*} userId
         */
        isDirectRoomWith(room, userId) {
          if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
            let other = room.getMember(userId);
            if (other) {
              if (room.getMyMembership() == "invite" && other.membership == "join") {
                return true;
              } else if (room.getMyMembership() == "join" && room.canInvite(this.currentUserId)) {
                return true;
              } else if (room.getMyMembership() == "join" && other.membership == "join") {
                return true;
              }
            }
          }
          return false;
        },

        /**
         * Return true if this room is a direct room with one other user. NOTE: this currently
         * only checks number of members, not any is_direct flag.
         * @param { } room
         */
        isDirectRoom(room) {
          // TODO - Use the is_direct accountData flag (m.direct). WE (as the client)
          // apprently need to set this...
          if (room && room.getJoinRule() == "invite" && room.getMembers().length == 2) {
            return true;
          }
          return false;
        },

        on(event, handler) {
          if (this.matrixClient) {
            this.matrixClient.on(event, handler);
          }
        },

        off(event, handler) {
          if (this.matrixClient) {
            this.matrixClient.off(event, handler);
          }
        },

        setUserDisplayName(name) {
          if (this.matrixClient) {
            return this.matrixClient
              .setDisplayName(name || this.user.userId)
              .then(() => (this.userDisplayName = name))
              .catch((err) => console.err("Failed to set display name", err));
          } else {
            return Promise.reject("No matrix client");
          }
        },

        setPassword(oldPassword, newPassword) {
          if (this.matrixClient && this.currentUser) {
            const authDict = {
              type: "m.login.password",
              identifier: {
                type: "m.id.user",
                user: this.currentUser.user_id,
              },
              // TODO: Remove `user` once servers support proper UIA
              // See https://github.com/matrix-org/synapse/issues/5665
              user: this.currentUser.user_id,
              password: oldPassword,
            };
            const self = this;
            return this.matrixClient
              .setPassword(authDict, newPassword)
              .then(() => {
                // Forget password and remove the 'is_guest' flag, we are now a "real" user!
                self.currentUser.password = undefined;
                self.currentUser.is_guest = false;
                self.$store.commit("setUser", self.currentUser);
              })
              .then(() => {
                return true;
              });
          }
          return Promise.resolve(false);
        },

        uploadFile(file, opts) {
          return this.matrixClient.uploadContent(file, opts);
        },

        /**
         * Get a matrix client that can be used for public queries. If we are logged in, this is the normal
         * matrix client. If not, we create a temp one with a temp password.
         * @returns A MatrixClient that can be used for public queries
         */
        getPublicQueryMatrixClient() {
          if (this.matrixClient) {
            return this.getMatrixClient().then(() => {
              return this.matrixClient;
            });
          } else {
            var tempUserString = this.$store.state.tempuser;
            var tempUser = null;
            if (tempUserString) {
              tempUser = JSON.parse(tempUserString);
            }
            return util.getMatrixBaseUrl(tempUser, this.$config).then((baseUrl) => {
              const tempMatrixClient = sdk.createClient({ baseUrl: baseUrl });

              var clientPromise;

              // Need to create an account?
              //
              if (tempUser) {
                clientPromise = Promise.resolve(tempUser);
              } else {
                const user = util.randomUser(this.$config.userIdPrefix);
                const pass = util.randomPass();
                clientPromise = tempMatrixClient
                  .register(user, pass, null, {
                    type: "m.login.dummy",
                    initial_device_display_name: this.$config.appName,
                  })
                  .then((response) => {
                    console.log("Response", response);
                    response.password = pass;
                    response.is_guest = true;
                    this.$store.commit("setTempUser", response);
                    return response;
                  });
              }

              // Get an access token
              clientPromise = clientPromise.then((user) => {
                var data = {
                  user: User.localPart(user.user_id),
                  password: user.password,
                  type: "m.login.password",
                  initial_device_display_name: this.$config.appName,
                };
                if (user.device_id) {
                  data.device_id = user.device_id;
                }
                return tempMatrixClient.login("m.login.password", data);
              });

              // Then login
              //
              // Create a slimmed down client, without crypto. This one is
              // Only used to get public room info from.
              clientPromise = clientPromise.then((user) => {
                var opts = {
                  baseUrl: baseUrl,
                  userId: user.user_id,
                  accessToken: user.access_token,
                  timelineSupport: false,
                };
                var matrixClient = sdk.createClient(opts);
                matrixClient.startClient();
                return matrixClient;
              });

              return clientPromise;
            });
          }
        },

        getPublicUserInfo(userId) {
          if (!userId) {
            return Promise.reject("Invalid parameters");
          }

          const parts = userId.split(":");
          if (parts.length != 2) {
            return Promise.reject("Unknown home server");
          }

          const clientPromise = this.getPublicQueryMatrixClient();
          let matrixClient;
          return clientPromise
            .then((client) => {
              matrixClient = client;
              return client.getProfileInfo(userId);
            })
            .then((response) => {
              if (response.avatar_url) {
                response.avatar = matrixClient.mxcUrlToHttp(response.avatar_url, 80, 80, "scale", true);
              }
              return Promise.resolve(response);
            })
            .catch((err) => {
              return Promise.reject("Failed to find user info: " + err);
            });
        },

        getPublicRoomInfo(roomId) {
          if (!roomId) {
            return Promise.reject("Invalid parameters");
          }

          const parts = roomId.split(":");
          if (parts.length != 2) {
            return Promise.reject("Unknown room server");
          }
          const server = parts[1];

          const clientPromise = this.getPublicQueryMatrixClient();

          const findOrGetMore = function _findOrGetMore(client, response) {
            for (var room of response.chunk) {
              if (
                (roomId.startsWith("#") && room.canonical_alias == roomId) ||
                (roomId.startsWith("!") && room.room_id == roomId)
              ) {
                if (room.avatar_url) {
                  room.avatar = client.mxcUrlToHttp(room.avatar_url, 80, 80, "scale", true);
                }
                return Promise.resolve(room);
              }
            }
            if (response.next_batch) {
              return client
                .publicRooms({
                  server: server,
                  limit: 1000,
                  since: response.next_batch,
                })
                .then((response) => {
                  return _findOrGetMore(client, response);
                })
                .catch((err) => {
                  return Promise.reject("Failed to find room: " + err);
                });
            } else {
              return Promise.reject("No more data");
            }
          };

          var matrixClient;
          return clientPromise
            .then((client) => {
              matrixClient = client;
              return matrixClient.publicRooms({ server: server, limit: 1000 });
            })
            .then((response) => {
              return findOrGetMore(matrixClient, response);
            })
            .catch((err) => {
              return Promise.reject("Failed to find room: " + err);
            });
        },

        updateNotificationCount() {
          var count = 0;
          this.rooms.forEach((room) => {
            count += room.getUnreadNotificationCount("total") || 0;
          });
          this.notificationCount = count;
        },

        setEventPinned(room, event, pinned) {
          if (room && room.currentState && event) {
            const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
            const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
            let pinnedEvents = content["pinned"] || [];
            if (pinned && !pinnedEvents.includes(event.getId())) {
              pinnedEvents.push(event.getId());
            } else if (!pinned && pinnedEvents.includes(event.getId())) {
              pinnedEvents = pinnedEvents.filter((e) => e != event.getId());
            } else {
              return; // no change
            }
            content.pinned = pinnedEvents;
            this.matrixClient.sendStateEvent(room.roomId, "m.room.pinned_events", content);
          }
        },

        getPinnedEvents(room) {
          if (room && room.currentState) {
            const pinnedMessagesEvent = room.currentState.getStateEvents("m.room.pinned_events", "");
            const content = pinnedMessagesEvent ? pinnedMessagesEvent.getContent() : {}
            return content["pinned"] || [];
          } else {
            return [];
          }
        },
      },
    });

    sdk.setCryptoStoreFactory(matrixService.createCryptoStore.bind(matrixService));

    Vue.prototype.$matrix = matrixService;
  },
};
