<template>
  <div class="chat-root">
    <div class="chat-root export d-flex flex-column" ref="exportRoot">
      <!-- Header-->
      <v-container fluid class="chat-header flex-grow-0 flex-shrink-0">
        <v-row class="chat-header-row flex-nowrap">
          <v-col class="chat-header-name ma-0 pa-0 flex-shrink-1 flex-nowrap">
            <div class="room-name-inline text-truncate" :title="room.name">
              {{ room.name }}
            </div>
            <div class="num-members">{{ $tc("room.members", room.getJoinedMemberCount()) }}</div>
          </v-col>

          <v-col cols="auto" class="text-end ma-0 pa-0">{{ exportDate }}</v-col>
        </v-row>
      </v-container>

      <div class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer">
        <div v-for="(event, index) in events" :key="event.getId()" :eventId="event.getId()">
          <!-- DAY Marker, shown for every new day in the timeline -->
          <div v-if="showDayMarkerBeforeEvent(event)" class="day-marker">
            <div class="line"></div>
            <div class="text">{{ dayForEvent(event) }}</div>
            <div class="line"></div>
          </div>

          <div v-if="!event.isRelation() && !event.isRedacted() && !event.isRedaction()" :ref="event.getId()">
            <div class="message-wrapper">
              <component :is="componentForEvent(event, true)" :room="room" :originalEvent="event"
                :nextEvent="events[index + 1]" :timelineSet="timelineSet" :componentFn="componentForEventForExport"
                ref="exportedEvent" v-on:layout-change="onLayoutChange" />
              <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
              <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- Loading indicator -->
    <v-container fluid fill-height class="exporting-indicator">
      <v-row align="center" justify="center">
        <v-col class="text-center">
          <v-progress-circular indeterminate color="primary"></v-progress-circular>
          <div>{{ statusText }}</div>
          <v-btn color="black" depressed class="filled-button mt-5" @click.stop="cancelExport">{{
            $t("menu.cancel")
          }}</v-btn>
        </v-col>
      </v-row>
    </v-container>
  </div>
</template>

<script>
import Vue from "vue";
import MessageIncomingText from "./messages/MessageIncomingText.vue";
import MessageIncomingFile from "./messages/MessageIncomingFile.vue";
import MessageIncomingImage from "./messages/MessageIncomingImage.vue";
import MessageIncomingAudio from "./messages/MessageIncomingAudio.vue";
import MessageIncomingVideo from "./messages/MessageIncomingVideo.vue";
import MessageIncomingSticker from "./messages/MessageIncomingSticker.vue";
import MessageOutgoingText from "./messages/MessageOutgoingText.vue";
import MessageOutgoingFile from "./messages/MessageOutgoingFile.vue";
import MessageOutgoingImage from "./messages/MessageOutgoingImage.vue";
import MessageOutgoingAudio from "./messages/MessageOutgoingAudio.vue";
import MessageOutgoingVideo from "./messages/MessageOutgoingVideo.vue";
import MessageOutgoingSticker from "./messages/MessageOutgoingSticker.vue";
import MessageOutgoingPoll from "./messages/MessageOutgoingPoll.vue";
import ContactJoin from "./messages/ContactJoin.vue";
import ContactLeave from "./messages/ContactLeave.vue";
import ContactInvited from "./messages/ContactInvited.vue";
import ContactChanged from "./messages/ContactChanged.vue";
import RoomCreated from "./messages/RoomCreated.vue";
import RoomAliased from "./messages/RoomAliased.vue";
import RoomNameChanged from "./messages/RoomNameChanged.vue";
import RoomTopicChanged from "./messages/RoomTopicChanged.vue";
import RoomAvatarChanged from "./messages/RoomAvatarChanged.vue";
import RoomHistoryVisibility from "./messages/RoomHistoryVisibility.vue";
import RoomJoinRules from "./messages/RoomJoinRules.vue";
import RoomPowerLevelsChanged from "./messages/RoomPowerLevelsChanged.vue";
import RoomGuestAccessChanged from "./messages/RoomGuestAccessChanged.vue";
import RoomEncrypted from "./messages/RoomEncrypted.vue";
import RoomDeletionNotice from "./messages/RoomDeletionNotice.vue";
import DebugEvent from "./messages/DebugEvent.vue";
import MessageOperations from "./messages/MessageOperations.vue";
import AvatarOperations from "./messages/AvatarOperations.vue";
import ChatHeader from "./ChatHeader.vue";
import VoiceRecorder from "./VoiceRecorder.vue";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet.vue";
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet.vue";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet.vue";
import BottomSheet from "./BottomSheet.vue";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
import util from "../plugins/utils";
import JSZip from "jszip";
import { saveAs } from "file-saver";
import { EventTimelineSet } from "matrix-js-sdk";
import axios from 'axios';

export default {
  name: "RoomExport",
  mixins: [chatMixin],
  components: {
    ChatHeader,
    MessageIncomingText,
    MessageIncomingFile,
    MessageIncomingImage,
    MessageIncomingAudio,
    MessageIncomingVideo,
    MessageIncomingSticker,
    MessageOutgoingText,
    MessageOutgoingFile,
    MessageOutgoingImage,
    MessageOutgoingAudio,
    MessageOutgoingVideo,
    MessageOutgoingSticker,
    MessageOutgoingPoll,
    ContactJoin,
    ContactLeave,
    ContactInvited,
    ContactChanged,
    RoomCreated,
    RoomAliased,
    RoomNameChanged,
    RoomTopicChanged,
    RoomAvatarChanged,
    RoomHistoryVisibility,
    RoomJoinRules,
    RoomPowerLevelsChanged,
    RoomGuestAccessChanged,
    RoomEncrypted,
    RoomDeletionNotice,
    DebugEvent,
    MessageOperations,
    VoiceRecorder,
    RoomInfoBottomSheet,
    CreatedRoomWelcomeHeader,
    MessageOperationsBottomSheet,
    StickerPickerBottomSheet,
    BottomSheet,
    AvatarOperations,
    CreatePollDialog,
  },
  props: {
    room: {
      type: Object,
      default: function () {
        return null;
      },
    },
  },
  data() {
    return {
      timelineSet: null,
      events: [],
      fetchedEvents: 0,
      totalEvents: 0,
      processedEvents: 0,
      statusText: "",
      cancelled: false,
    };
  },
  mounted() {
    this.$nextTick(() => {
      this.doExport();
    });
  },
  watch: {
    processedEvents() {
      this.statusText = this.$t("export.processed_n_of_total_events", {
        count: this.processedEvents,
        total: this.totalEvents,
      });
    },
  },
  computed: {
    exportDate() {
      return this.$t("export.exported_date", { date: util.formatDay(Date.now().valueOf()) });
    },
  },
  methods: {
    componentForEventForExport(event) {
      return this.componentForEvent(event, true);
    },
    cancelExport() {
      this.cancelled = true;
    },
    async getEvents() {
      const eventsPerBatch = 100;
      let batchToken = null;
      var nToFetch = null;
      this.totalEvents = nToFetch == null ? 0 : nToFetch;
      var fetchedEvents = [];
      const eventMapper = this.$matrix.matrixClient.getEventMapper();

      while (nToFetch == null || nToFetch > 0) {
        const result = await this.$matrix.matrixClient.createMessagesRequest(
          this.room.roomId,
          batchToken,
          nToFetch == null ? eventsPerBatch : Math.min(nToFetch, eventsPerBatch),
          "b"
        );
        // For testing, uncomment to give a chance to cancel...
        // await new Promise((resolve, ignoredReject) => {
        //   setTimeout(() => {
        //     resolve(true);
        //   }, 1000);
        // });
        if (this.cancelled) {
          return Promise.reject("cancelled");
        }
        if (result.chunk.length === 0) break;
        if (nToFetch != null) {
          nToFetch -= result.chunk.length;
          this.statusText = this.$t("export.fetched_n_of_total_events", {
            count: this.totalEvents - nToFetch,
            total: this.totalEvents,
          });
        } else {
          this.totalEvents += result.chunk.length;
          this.statusText = this.$t("export.fetched_n_events", { count: this.totalEvents });
        }
        fetchedEvents.push(...result.chunk.map(eventMapper));

        if (!result.end) break;
        batchToken = result.end;
      }
      return fetchedEvents;
    },
    doExport() {
      var zip = null;
      var currentMediaSize = 0;
      var maxMediaSize = 1024 * 1024 * 1024; // 1GB

      this.getEvents()
        .then((events) => {
          var decryptionPromises = [];
          for (const event of this.events) {
            if (event.isEncrypted()) {
              decryptionPromises.push(
                this.$matrix.matrixClient.decryptEventIfNeeded(event, {
                  isRetry: true,
                  emit: false,
                })
              );
            }
          }
          return Promise.all(decryptionPromises).then(() => {
            return events;
          });
        })
        .then((events) => {
          // Create a timeline and add the events to that, so that relations etc are aggregated correctly!
          this.timelineSet = new EventTimelineSet(null, { unstableClientRelationAggregation: true });
          this.timelineSet.addEventsToTimeline(events.reverse(), true, this.timelineSet.getLiveTimeline(), "");
          this.events = events;

          // Need to set thread root events and replyEvents so stuff is rendered correctly.
          this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => {
            const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
            if (parentEvent) {
              Vue.set(parentEvent, "isMxThread", true);
              Vue.set(event, "parentThread", parentEvent);
            } 
          });
          this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => {
            const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
            if (parentEvent) {
              Vue.set(event, "replyEvent", parentEvent);
            }
          });

          // Wait a tick so UI is updated.
          return new Promise((resolve, ignoredReject) => {
            this.$nextTick(() => {
              resolve(true);
            });
          });
        })
        .then(() => {
          // UI updated, start processing events
          zip = new JSZip();
          var avatarFolder = zip.folder("avatars");
          var imageFolder = zip.folder("images");
          var audioFolder = zip.folder("audio");
          var videoFolder = zip.folder("video");

          var downloadPromises = [];
          let components = this.$refs.exportedEvent;
          for (const parentComp of components) {
            let childComponents = [parentComp];

            // Some components, i.e. the media threads, have subcomponents
            // that we want to export. So pickup subcomponents here as well.
            if (parentComp.$refs && parentComp.$refs.exportedEvent) {
              if (Array.isArray(parentComp.$refs.exportedEvent)) {
                for (const child of parentComp.$refs.exportedEvent) {
                  childComponents.push(child);
                }
              } else {
                childComponents.push(parentComp.$refs.exportedEvent);
              }
            }
            for (const comp of childComponents) {

              // Avatars need downloading?
              if (comp.$el) {
                const avatars = comp.$el.getElementsByClassName("v-avatar");
                if (avatars && avatars.length > 0) {
                  const member = this.room.getMember(comp.event.getSender());
                  if (member) {
                    const fileName = comp.event.getSender() + ".png";

                    const setSource = (fileName) => {
                      for (let avatarIndex = 0; avatarIndex < avatars.length; avatarIndex++) {
                        const avatarElement = avatars[avatarIndex];
                        const images = avatarElement.getElementsByTagName("img");
                        for (let imageIndex = 0; imageIndex < images.length; imageIndex++) {
                          const img = images[imageIndex];
                          img.onerror = undefined;
                          img.src = './avatars/' + fileName;
                        }
                      }
                    }

                    if (!avatarFolder.file(fileName)) {
                      const url = member.getAvatarUrl(this.$matrix.matrixClient.getHomeserverUrl(), 40, 40, "scale", true);
                      if (url) {
                        avatarFolder.file(fileName, "empty");
                        downloadPromises.push(
                          axios.get(url, {
                            responseType: 'blob'
                          })
                            .then(result => {
                              if (result.data) {
                                avatarFolder.file(fileName, result.data);
                                setSource(fileName);
                              }
                            })
                            .catch(err => {
                              console.error("Download error: ", err);
                              avatarFolder.remove(fileName);
                            }));
                      }
                    } else {
                      setSource(fileName);
                    }
                  }
                }
              }

              let componentClass = comp.$vnode.tag.split("-").reverse()[0];
              switch (componentClass) {
                case "MessageIncomingImageExport":
                case "MessageOutgoingImageExport":
                  // TODO - maybe consider what media to download based on the file size we already have?
                  // info = comp.event.getContent().info;
                  // if (info && info.size && currentMediaSize + info.size > maxMediaSize) {
                  //   // No need to even download.
                  //   console.log("Dont download!");
                  //   continue;
                  // }

                  downloadPromises.push(
                    util
                      .getAttachment(this.$matrix.matrixClient, comp.event, null, true)
                      .then((blob) => {
                        return new Promise((resolve, ignoredReject) => {
                          let mime = blob.type;
                          var extension = ".png";
                          switch (mime) {
                            case "image/jpeg":
                            case "image/jpg":
                              extension = ".jpg";
                              break;
                            case "image/gif":
                              extension = ".gif";
                          }
                          if (currentMediaSize + blob.size <= maxMediaSize) {
                            currentMediaSize += blob.size;

                            let fileName = comp.event.getId() + extension;
                            imageFolder.file(fileName, blob); // TODO calc bytes

                            let blobUrl = URL.createObjectURL(blob);
                            comp.src = blobUrl;

                            this.$nextTick(() => {
                              // Update source
                              let elements = comp.$el.getElementsByClassName("v-image__image");
                              let element = elements && elements[0];
                              if (element) {
                                element.style.backgroundImage = 'url("./images/' + fileName + '")';
                                element.classList.remove("v-image__image--preload");
                              }
                              URL.revokeObjectURL(blobUrl); // Give the blob back
                              this.processedEvents += 1;
                              resolve(true);
                            });
                          }
                        });
                      })
                      .catch((ignoredErr) => {
                        this.processedEvents += 1;
                      })
                  );
                  break;
                case "MessageIncomingAudioExport":
                case "MessageOutgoingAudioExport":
                  downloadPromises.push(
                    util
                      .getAttachment(this.$matrix.matrixClient, comp.event, null, true)
                      .then((blob) => {
                        if (currentMediaSize + blob.size <= maxMediaSize) {
                          currentMediaSize += blob.size;
                          return new Promise((resolve, ignoredReject) => {
                            //let mime = blob.type;
                            var extension = ".mp3";
                            let fileName = comp.event.getId() + extension;
                            audioFolder.file(fileName, blob); // TODO calc bytes
                            let elements = comp.$el.getElementsByTagName("audio");
                            let element = elements && elements[0];
                            if (element) {
                              element.src = "./audio/" + fileName;
                            }
                            this.processedEvents += 1;
                            resolve(true);
                          });
                        }
                      })
                      .catch((ignoredErr) => {
                        this.processedEvents += 1;
                      })
                  );
                  break;
                case "MessageIncomingVideoExport":
                case "MessageOutgoingVideoExport":
                  downloadPromises.push(
                    util
                      .getAttachment(this.$matrix.matrixClient, comp.event, null, true)
                      .then((blob) => {
                        if (currentMediaSize + blob.size <= maxMediaSize) {
                          currentMediaSize += blob.size;
                          return new Promise((resolve, ignoredReject) => {
                            //let mime = blob.type;
                            var extension = ".mp4";
                            let fileName = comp.event.getId() + extension;
                            videoFolder.file(fileName, blob); // TODO calc bytes
                            let elements = comp.$el.getElementsByTagName("video");
                            let element = elements && elements[0];
                            if (element) {
                              element.src = "./video/" + fileName;
                            }
                            this.processedEvents += 1;

                            resolve(true);
                          });
                        }
                      })
                      .catch((ignoredErr) => {
                        this.processedEvents += 1;
                      })
                  );
                  break;
                default:
                  this.processedEvents += 1;
                  break;
              }
            }
          }
          return Promise.all(downloadPromises);
        })
        .then(() => {
          console.log("All media added, total size: " + currentMediaSize);

          let root = this.$refs.exportRoot;

          var doc = "<!DOCTYPE html>\n<html><head>\n<meta charset=\"utf-8\"/>\n";

          for (const sheet of document.styleSheets) {
            doc += "<style type='text/css'>\n";
            for (const rule of sheet.cssRules) {
              if (rule.constructor.name != "CSSFontFaceRule") {
                // Strip font face rules for now.
                doc += rule.cssText + "\n";
              }
            }
            doc += "</style>\n";
          }
          doc +=
            "</head><body><div class='v-application v-application--is-ltr theme--light' style='height:100%;overflow-y:auto'>";
          const getCssRules = function (el) {
            if (el.classList.contains("op-button")) {
              el.innerHTML = "";
            } else {
              for (let i = 0; i < el.children.length; i++) {
                getCssRules(el.children[i]);
              }
            }
          };
          getCssRules(root);

          this.$nextTick(() => {
            doc += this.$refs.exportRoot.outerHTML;
            doc += "</div></body></html>";

            zip.file("chat.html", doc);
            zip.generateAsync({ type: "blob" }).then((content) => {
              saveAs(
                content,
                this.$t("export.export_filename", { date: util.formatDay(Date.now().valueOf()) }) + ".zip"
              );
              this.status = "";
              this.$emit("close");
            });
          });
        })
        .catch((err) => {
          console.error("Failed to export:", err);
          this.$emit("close");
        });
    },
    onLayoutChange(action, ignoredelement) {
      action();
    },
  },
};
</script>

<style lang="scss">
.chat-root.export {
  .messageIn-thread, .messageOut-thread {
    /** For media threads, hide all duplicated metadata, like
    sender, sender avatar, time, quick reactions etc. They are
    shown for the root thread event */
    .messageIn {
      margin-left: 50px !important;
    }
    .messageOut {
      margin-right: 50px !important;
    }
    .messageIn, .messageOut {
      .quick-reaction-container, .senderAndTime, .avatar {
        display: none;
      }
    }
  }
}
</style>