<script>

import { marked } from 'marked';

import { fetcher, Dialog, DirectoryView, TopBar, Breadcrumb, BottomBar, Button, InputDialog, MainLayout, Menu, Notification, FileUploader, Spinner } from 'pankow';
import { sanitize, sleep } from 'pankow/utils';

import { ISTATES } from '../constants.js';

import PreviewPanel from '../components/PreviewPanel.vue';
import { createDirectoryModel } from '../models/DirectoryModel.js';

const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
const BASE_URL = import.meta.env.BASE_URL || '/';

const beforeUnloadListener = (event) => {
  event.preventDefault();
  return window.confirm('File operation still in progress. Really close?');
};

export default {
  name: 'FolderView',
  components: {
    BottomBar,
    Button,
    Dialog,
    DirectoryView,
    FileUploader,
    InputDialog,
    MainLayout,
    Menu,
    Notification,
    Breadcrumb,
    PreviewPanel,
    Spinner,
    TopBar
  },
  data() {
    return {
      busy: true,
      fallbackIcon: `${BASE_URL}mime-types/none.svg`,
      cwd: '/',
      busyRefresh: false,
      busyRestart: false,
      fatalError: false,
      footerContent: '',
      activeItem: null,
      activeDirectoryItem: {},
      items: [],
      selectedItems: [],
      accessToken: localStorage.token,
      title: 'Cloudron',
      appLink: '',
      resourceType: '',
      resourceId: '',
      visible: true,
      uploadRequest: null,
      breadcrumbHomeItem: {
        label: '/app/data/',
        action: () => {
          this.cwd = '/';
        }
      },
      ownersModel: [],
      // contextMenuModel will have activeItem attached if any command() is called
      createMenuModel: [{
        label: this.$t('filemanager.toolbar.newFile'),
        icon: 'fa-solid fa-file-circle-plus',
        action: this.onNewFile
      }, {
        label: this.$t('filemanager.toolbar.newFolder'),
        icon: 'fa-solid fa-folder-plus',
        action: this.onNewFolder
      }],
      uploadMenuModel: [{
        label: this.$t('filemanager.toolbar.uploadFile'),
        icon: 'fa-solid fa-file-arrow-up',
        action: this.onUploadFile
      }, {
        label: this.$t('filemanager.toolbar.newFolder'),
        icon: 'fa-regular fa-folder-open',
        action: this.onUploadFolder
      }]
    };
  },
  computed: {
    breadcrumbItems() {
      const parts = this.cwd.split('/').filter((p) => !!p.trim())
      const crumbs = [];

      parts.forEach((p, i) => {
        crumbs.push({
          label: p,
          action: () => {
            this.cwd = '/' + parts.slice(0, i+1).join('/');
          }
        });
      });

      return crumbs;
    }
  },
  watch: {
    cwd(newCwd, oldCwd) {
      if (this.resourceType && this.resourceId) this.$router.push(`/home/${this.resourceType}/${this.resourceId}${this.cwd}`);
      this.loadCwd();
    }
  },
  async mounted() {
    this.busy = true;
    const type = this.$route.params.type || 'app';
    const resourceId = this.$route.params.resourceId;
    const cwd = this.$route.params.cwd;

    if (type === 'app') {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${resourceId}`, { access_token: this.accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) {
        console.error(`Invalid resource ${type} ${resourceId}`, error || result.status);
        return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
      }

      this.appLink = `https://${result.body.fqdn}`;
      this.title = `${result.body.label || result.body.fqdn} (${result.body.manifest.title})`;
    } else if (type === 'volume') {
      let error, result;
      try {
        result = await fetcher.get(`${API_ORIGIN}/api/v1/volumes/${resourceId}`, { access_token: this.accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 200) {
        console.error(`Invalid resource ${type} ${resourceId}`, error || result.status);
        return this.onFatalError(`Invalid resource ${type} ${resourceId}`);
      }

      this.title = result.body.name;
    } else {
      return this.onFatalError(`Unsupported type ${type}`);
    }

    try {
      const result = await fetcher.get(`${API_ORIGIN}/api/v1/dashboard/config`, { access_token: this.accessToken });
      this.footerContent = marked.parse(result.body.footer);
    } catch (e) {
      console.error('Failed to fetch Cloudron config.', e);
    }

    window.document.title = `File Manager - ${this.title}`;

    this.cwd = sanitize('/' + (cwd ? cwd.join('/') : '/'));
    this.resourceType = type;
    this.resourceId = resourceId;

    this.directoryModel = createDirectoryModel(API_ORIGIN, this.accessToken, type === 'volume' ? `volumes/${resourceId}` : `apps/${resourceId}`);
    this.ownersModel = this.directoryModel.ownersModel;

    this.loadCwd();

    this.$watch(() => this.$route.params, (toParams, previousParams) => {
      if (toParams.type !== 'app' && toParams.type !== 'volume') return this.onFatalError(`Unknown type ${toParams.type}`);

      if ((toParams.type !== this.resourceType) || (toParams.resourceId !== this.resourceId)) {
        this.resourceType = toParams.type;
        this.resourceId = toParams.resourceId;

        this.directoryModel = createDirectoryModel(API_ORIGIN, this.accessToken, toParams.type === 'volume' ? `volumes/${toParams.resourceId}` : `apps/${toParams.resourceId}`);
      }

      this.cwd = toParams.cwd ? `/${toParams.cwd.join('/')}` : '/';
    });
  },
  methods: {
    onFatalError(errorMessage) {
      this.fatalError = errorMessage;
      this.$refs.fatalErrorDialog.open();
    },
    onCancelUpload() {
      if (!this.uploadRequest || !this.uploadRequest.xhr) return;
      this.uploadRequest.xhr.abort();
    },
    // generic dialog focus handler
    onDialogShow(focusElementId) {
      setTimeout(() => document.getElementById(focusElementId).focus(), 0);
    },
    async onNewFile() {
      const newFileName = await this.$refs.inputDialog.prompt({
        message: this.$t('filemanager.newFileDialog.title'),
        value: '',
        confirmStyle: 'success',
        confirmLabel: this.$t('filemanager.newFileDialog.create'),
        rejectLabel: this.$t('main.dialog.cancel'),
        modal: false
      });

      if (!newFileName) return;

      await this.directoryModel.newFile(this.directoryModel.buildFilePath(this.cwd, newFileName));
      await this.loadCwd();
    },
    async onNewFolder() {
      const newFolderName = await this.$refs.inputDialog.prompt({
        message: this.$t('filemanager.newDirectoryDialog.title'),
        value: '',
        confirmStyle: 'success',
        confirmLabel: this.$t('filemanager.newFileDialog.create'),
        rejectLabel: this.$t('main.dialog.cancel'),
        modal: false
      });

      if (!newFolderName) return;

      await this.directoryModel.newFolder(this.directoryModel.buildFilePath(this.cwd, newFolderName));
      await this.loadCwd();
    },
    onUploadFile() {
      this.$refs.fileUploader.onUploadFile(this.cwd);
    },
    onUploadFolder() {
      this.$refs.fileUploader.onUploadFolder(this.cwd);
    },
    onUploadFinished() {
      this.loadCwd();
    },
    onAppChange(event) {
      this.$router.push(`/home/${event.value.type}/${event.value.id}`);
      this.cwd = '/';
      this.loadResource(event.value);
    },
    onSelectionChanged(items) {
      this.activeItem = items[0] || null;
      this.selectedItems = items;
    },
    onActivateBreadcrumb(path) {
      this.cwd = sanitize(path);
    },
    async onRefresh() {
      this.busyRefresh = true;
      await this.loadCwd();
      setTimeout(() => { this.busyRefresh = false; }, 500);
    },
    // either dataTransfer (external drop) or files (internal drag)
    async onDrop(targetFolder, dataTransfer, files) {
      const fullTargetFolder = sanitize(this.cwd + '/' + targetFolder);

      // if dataTransfer is set, we have a file/folder drop from outside
      if (dataTransfer) {

        async function getFile(entry) {
          return new Promise((resolve, reject) => {
            entry.file(resolve, reject);
          });
        }

        async function readEntries(dirReader) {
          return new Promise((resolve, reject) => {
            dirReader.readEntries(resolve, reject);
          });
        }

        const fileList = [];
        async function traverseFileTree(item) {
          if (item.isFile) {
            fileList.push(await getFile(item));
          } else if (item.isDirectory) {
            // Get folder contents
            const dirReader = item.createReader();
            const entries = await readEntries(dirReader);

            for (let i in entries) {
              await traverseFileTree(entries[i], item.name);
            }
          } else {
            console.log('Skipping uknown file type', item);
          }
        }

        // collect all files to upload
        for (const item of dataTransfer.items) {
          const entry = item.webkitGetAsEntry();

          if (entry.isFile) {
            fileList.push(await getFile(entry));
          } else if (entry.isDirectory) {
            await traverseFileTree(entry, sanitize(`${this.cwd}/${targetFolder}`));
          }
        }

        this.$refs.fileUploader.addFiles(fileList, sanitize(`${this.cwd}/${targetFolder}`));
      } else {
        if (!files.length) return;

        window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
        this.$refs.pasteInProgressDialog.open();

        // check ctrl for cut/copy
        await this.directoryModel.paste(fullTargetFolder, 'cut', files);
        await this.loadCwd();

        window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
        this.$refs.pasteInProgressDialog.close();
      }
    },
    onItemActivated(item) {
      if (!item) return;
      if (item.mimeType === 'inode/symlink') return;

      if (item.type === 'directory') this.cwd = sanitize(this.cwd + '/' + item.name);
      else this.$router.push(`/viewer/${this.resourceType}/${this.resourceId}${sanitize(this.cwd + '/' + item.name)}`);
    },
    async deleteHandler(files) {
      if (!files) return;

      const confirmed = await this.$refs.inputDialog.confirm({
        message: this.$t('filemanager.removeDialog.reallyDelete'),
        confirmStyle: 'danger',
        confirmLabel: this.$t('main.dialog.yes'),
        rejectLabel: this.$t('main.dialog.no'),
        modal: false
      });

      if (!confirmed) return;

      window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
      this.$refs.deleteInProgressDialog.open();

      for (let i in files) {
        try {
          await this.directoryModel.remove(this.directoryModel.buildFilePath(this.cwd, files[i].name));
        } catch (e) {
          console.error(`Failed to remove file ${files[i].name}:`, e);
        }
      }

      await this.loadCwd();

      window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
      this.$refs.deleteInProgressDialog.close();
    },
    async renameHandler(file, newName) {
      if (file.name === newName) return;

      try {
        await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName));
        await this.loadCwd();
      } catch (e) {
        if (e.status === 409) {
          const confirmed = await this.$refs.inputDialog.confirm({
            message: this.$t('filemanager.renameDialog.reallyOverwrite'),
            confirmStyle: 'danger',
            confirmLabel: this.$t('main.dialog.yes'),
            rejectLabel: this.$t('main.dialog.no')
          });

          if (!confirmed) return;

          await this.directoryModel.rename(this.directoryModel.buildFilePath(this.cwd, file.name), sanitize(this.cwd + '/' + newName), true /* overwrite */);
          await this.loadCwd();
        }
        else console.error(`Failed to rename ${file} to ${newName}`, e);
      }
    },
    async changeOwnerHandler(files, newOwnerUid) {
      if (!files) return;

      for (let i in files) {
        await this.directoryModel.chown(this.directoryModel.buildFilePath(this.cwd, files[i].name), newOwnerUid);
      }

      await this.loadCwd();
    },
    async pasteHandler(action, files, target) {
      if (!files || !files.length) return;

      const targetPath = (target && target.isDirectory) ? sanitize(this.cwd + '/' + target.fileName) : this.cwd;

      window.addEventListener('beforeunload', beforeUnloadListener, { capture: true });
      this.$refs.pasteInProgressDialog.open();

      try {
        await this.directoryModel.paste(targetPath, action, files);
      } catch (e) {
        window.pankow.notify({ type: 'danger', text: e, persistent: true })
      }

      this.clipboard = {};
      await this.loadCwd();

      window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
      this.$refs.pasteInProgressDialog.close();
    },
    async downloadHandler(file) {
      await this.directoryModel.download(this.directoryModel.buildFilePath(this.cwd, file.name));
    },
    async extractHandler(file) {
      this.$refs.extractInProgressDialog.open();
      await this.directoryModel.extract(this.directoryModel.buildFilePath(this.cwd, file.name));
      await this.loadCwd();
      this.$refs.extractInProgressDialog.close();
    },
    async uploadHandler(targetDir, file, progressHandler) {
      this.uploadRequest = this.directoryModel.upload(targetDir, file, progressHandler);

      try {
        await this.uploadRequest;
      } catch (e) {
        console.log('Upload cancelled.', e);
      }

      this.uploadRequest = null;

      await this.loadCwd();
    },
    async loadCwd() {
      this.items = await this.directoryModel.listFiles(this.cwd);

      const tmp = this.cwd.split('/').slice(1);
      let name = '';
      if (tmp.length >= 1 && tmp[tmp.length-1]) name = tmp[tmp.length-1];

      this.activeDirectoryItem = {
        id: name,
        name: name,
        type: 'directory',
        mimeType: 'inode/directory',
        icon: `${BASE_URL}mime-types/inode-directory.svg`
      };

      this.busy = false;
    },
    async onRestartApp() {
      if (this.resourceType !== 'app') return;

      const confirmed = await this.$refs.inputDialog.confirm({
        message: this.$t('filemanager.toolbar.restartApp') + '?',
        confirmStyle: 'danger',
        confirmLabel: this.$t('main.dialog.yes'),
        rejectLabel: this.$t('main.dialog.no')
      });

      if (!confirmed) return;

      this.busyRestart = true;

      let error, result;
      try {
        result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${this.resourceId}/restart`, null, { access_token: this.accessToken });
      } catch (e) {
        error = e;
      }

      if (error || result.status !== 202) {
        console.error(`Failed to restart app ${this.resourceId}`, error || result.status);
        return;
      }

      while(true) {
        let result;
        try {
          result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${this.resourceId}`, { access_token: this.accessToken });
        } catch (e) {
          console.error('Failed to fetch app status.', e);
        }

        if (result && result.status === 200 && result.body.installationState === ISTATES.INSTALLED) break;

        await sleep(2000);
      }

      this.busyRestart = false;
    }
  }
};

</script>

<template>
  <MainLayout>
    <template #dialogs>
      <Notification />

      <Dialog ref="fatalErrorDialog" modal title="Error">
        <p>{{ fatalError }}</p>
      </Dialog>

      <Dialog ref="extractInProgressDialog" modal :title="$t('filemanager.extractionInProgress')">
        <div style="text-align: center;">
          <Spinner style="margin: 10px; width: 50px; height: 50px"/>
        </div>
      </Dialog>

      <Dialog ref="pasteInProgressDialog" modal :title="$t('filemanager.pasteInProgress')">
        <div style="text-align: center;">
          <Spinner style="margin: 10px; width: 50px; height: 50px"/>
        </div>
      </Dialog>

      <Dialog ref="deleteInProgressDialog" modal :title="$t('filemanager.deleteInProgress')">
        <div style="text-align: center;">
          <Spinner style="margin: 10px; width: 50px; height: 50px"/>
        </div>
      </Dialog>

      <InputDialog ref="inputDialog" />
    </template>
    <template #header>
      <TopBar class="navbar">
        <template #left>
          <Button icon="fa-solid fa-arrow-rotate-right" :loading="busyRefresh" @click="onRefresh()" secondary plain tool/>
          <Breadcrumb :home="breadcrumbHomeItem" :items="breadcrumbItems" :activate-handler="onActivateBreadcrumb"/>
        </template>
        <template #right>
          <Button icon="fa-solid fa-plus" :menu="createMenuModel">{{ $t('filemanager.toolbar.new') }}</Button>
          <Button icon="fa-solid fa-upload" :menu="uploadMenuModel">{{ $t('filemanager.toolbar.upload') }}</Button>

          <Button style="margin-left: 20px;" :title="$t('filemanager.toolbar.restartApp')" secondary tool :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp" v-show="resourceType === 'app'"/>
          <Button :href="'/terminal.html?id=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-terminal" :title="$t('terminal.title')" />
          <Button :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-align-left" :title="$t('logs.title')" />
        </template>
      </TopBar>
    </template>
    <template #body>
      <div class="main-view">
        <div class="main-view-col">
          <DirectoryView
            class="directory-view"
            :busy="busy"
            :show-owner="true"
            :show-size="true"
            :show-modified="true"
            @selection-changed="onSelectionChanged"
            @item-activated="onItemActivated"
            :delete-handler="deleteHandler"
            :rename-handler="renameHandler"
            :change-owner-handler="changeOwnerHandler"
            :paste-handler="pasteHandler"
            :download-handler="downloadHandler"
            :extract-handler="extractHandler"
            :new-file-handler="onNewFile"
            :new-folder-handler="onNewFolder"
            :upload-file-handler="onUploadFile"
            :upload-folder-handler="onUploadFolder"
            :drop-handler="onDrop"
            :items="items"
            :owners-model="ownersModel"
            :fallback-icon="fallbackIcon"
            :tr="$t"
          />
        </div>
        <div class="main-view-col" style="max-width: 300px;">
          <div class="side-bar-title">
            <a v-show="appLink" :href="appLink" target="_blank">{{ title }}</a>
            <span v-show="!appLink">{{ title }}</span>
          </div>
          <PreviewPanel :item="activeItem || activeDirectoryItem" :fallback-icon="fallbackIcon"/>
        </div>
      </div>
    </template>
    <template #footer>
      <FileUploader
        ref="fileUploader"
        :upload-handler="uploadHandler"
        :cancel-handler="onCancelUpload"
        @finished="onUploadFinished"
        :tr="$t"
      />
      <BottomBar>
        <div v-html="footerContent" class="bottom-bar-content"></div>
      </BottomBar>
    </template>
  </MainLayout>
</template>

<style scoped>

.main-view {
  flex-grow: 1;
  overflow: hidden;
  height: 100%;
  display: flex;
  padding: 0 10px;
}

.side-bar-title {
  text-align: center;
  font-size: 20px;
  margin-bottom: 20px;
}

.main-view-col {
  flex-grow: 1;
}

.directory-view {
  background-color: var(--pankow-color-background);
}

</style>

<style>

.bottom-bar-content > p {
  margin: 0;
}

</style>
