<script setup>

import { useI18n } from 'vue-i18n';
const i18n = useI18n();
const t = i18n.t;

import { ref, onMounted, useTemplateRef } from 'vue';
import { useRouter, useRoute, onBeforeRouteUpdate } from 'vue-router';
import { fetcher, Dialog, DirectoryView, TreeView, TopBar, Button, InputDialog, MainLayout, ButtonGroup, Notification, FileUploader, Spinner } from '@cloudron/pankow';
import { sanitize, sleep } from '@cloudron/pankow/utils';
import { API_ORIGIN, BASE_URL, ISTATES } from '../constants.js';
import PreviewPanel from '../components/PreviewPanel.vue';
import { createDirectoryModel } from '../models/DirectoryModel.js';

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

const router = useRouter();
const route = useRoute();

let directoryModel;
const accessToken = localStorage.token;

const fatalErrorDialog = useTemplateRef('fatalErrorDialog');
const inputDialog = useTemplateRef('inputDialog');
const fileUploader = useTemplateRef('fileUploader');
const pasteInProgressDialog = useTemplateRef('pasteInProgressDialog');
const deleteInProgressDialog = useTemplateRef('deleteInProgressDialog');
const extractInProgressDialog = useTemplateRef('extractInProgressDialog');

const busy = ref(true);
const fallbackIcon = ref(`${BASE_URL}mime-types/none.svg`);
const cwd = ref('/');
const busyRestart = ref(false);
const fatalError = ref(false);
const activeItem = ref(null);
const activeDirectoryItem = ref({});
const items = ref([]);
const selectedItems = ref([]);
const title = ref('Cloudron');
const appLink = ref('');
const resourceType = ref('');
const resourceId = ref('');
const uploadRequest = ref(null);
const ownersModel = ref([]);

// contextMenuModel will have activeItem attached if any command() is called
const createMenuModel = [{
  icon: 'fa-solid fa-file-circle-plus',
  label: t('filemanager.toolbar.newFile'),
  action: onNewFile,
}, {
  icon: 'fa-solid fa-folder-plus',
  label: t('filemanager.toolbar.newFolder'),
  action: onNewFolder,
}];

const uploadMenuModel = [{
  icon: 'fa-solid fa-file-arrow-up',
  label: t('filemanager.toolbar.uploadFile'),
  action: onUploadFile,
}, {
  icon: 'fa-regular fa-folder-open',
  label: t('filemanager.toolbar.uploadFolder'),
  action: onUploadFolder,
}];

function onFatalError(errorMessage) {
  fatalError.value = errorMessage;
  fatalErrorDialog.value.open();
}

function onCancelUpload() {
  if (!uploadRequest.value || !uploadRequest.value.xhr) return;
  uploadRequest.value.xhr.abort();
}

async function onNewFile() {
  const newFileName = await inputDialog.value.prompt({
    message: t('filemanager.newFileDialog.title'),
    value: '',
    required: true,
    confirmStyle: 'primary',
    confirmLabel: t('filemanager.newFileDialog.create'),
    rejectLabel: t('main.dialog.cancel'),
    rejectStyle: 'secondary'
  });

  if (!newFileName) return;

  await directoryModel.newFile(directoryModel.buildFilePath(cwd.value, newFileName));
  await loadCwd();
}

async function onNewFolder() {
  const newFolderName = await inputDialog.value.prompt({
    message: t('filemanager.newDirectoryDialog.title'),
    value: '',
    required: true,
    confirmStyle: 'primary',
    confirmLabel: t('filemanager.newFileDialog.create'),
    rejectLabel: t('main.dialog.cancel'),
    rejectStyle: 'secondary'
  });

  if (!newFolderName) return;

  await directoryModel.newFolder(directoryModel.buildFilePath(cwd.value, newFolderName));
  await loadCwd();
}

function onUploadFile() {
  fileUploader.value.onUploadFile(cwd.value);
}

function onUploadFolder() {
  fileUploader.value.onUploadFolder(cwd.value);
}

async function onUploadFinished() {
  await loadCwd();
}

function onSelectionChanged(items) {
  activeItem.value = items[0] || null;
  selectedItems.value = items;
}

function onTreeNavigate(event) {
  router.push(`/home/${resourceType.value}/${resourceId.value}${sanitize(event.path)}`);
}

async function onTreeDrop(targetPath, event) {
  // check if this is an internal pankow drag (files from DirectoryView)
  if (event.dataTransfer.getData('application/x-pankow') === 'selected') {
    const files = selectedItems.value;
    if (!files || !files.length) return;

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

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

    await loadCwd();

    window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
    pasteInProgressDialog.value.close();
  }
}

function treeListFiles(path) {
  if (!directoryModel) return [];
  return directoryModel.listFiles(path);
}

async function onRefresh() {
  await loadCwd();
}

// either dataTransfer (external drop) or files (internal drag)
async function onDrop(targetFolder, dataTransfer, files) {
  const fullTargetFolder = sanitize(cwd.value + '/' + 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);
      });
    }

    function setRelativePath(file, entry) {
      const relativePath = (entry.fullPath || entry.name || '').replace(/^\//, '');
      if (relativePath) {
        // trying with defineProperty() to better mimic native behavior adding a non-enumeratible property
        try {
          Object.defineProperty(file, 'webkitRelativePath', { value: relativePath });
        } catch {
          file.webkitRelativePath = relativePath;
        }
      }
    }


    // wrapper as chrome only returns files in batches of 100 entries
    async function readAllEntries(dirReader) {
      const all = [];
      let batch;
      do {
        batch = await new Promise((resolve, reject) => {
          dirReader.readEntries(resolve, reject);
        });
        all.push(...batch);
      } while (batch.length > 0);
      return all;
    }

    const fileList = [];
    async function traverseFileTree(item) {
      if (item.isFile) {
        const file = await getFile(item);
        setRelativePath(file, item);
        fileList.push(file);
      } else if (item.isDirectory) {
        const dirReader = item.createReader();
        const entries = await readAllEntries(dirReader);

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

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

      if (!entry) {
        const file = item.getAsFile ? item.getAsFile() : null;
        if (file) fileList.push(file);
        continue;
      }

      if (entry.isFile) {
        const file = await getFile(entry);
        setRelativePath(file, entry);
        fileList.push(file);
      } else if (entry.isDirectory) {
        await traverseFileTree(entry);
      }
    }

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

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

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

    window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
    pasteInProgressDialog.value.close();
  }
}

function onItemActivated(item) {
  if (!item) return;
  if (item.mimeType === 'inode/symlink') return;

  if (item.type === 'directory') router.push(`/home/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`);
  else router.push(`/viewer/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`);
}

async function deleteHandler(files) {
  if (!files) return;

  const confirmed = await inputDialog.value.confirm({
    message: t('filemanager.removeDialog.reallyDelete'),
    confirmStyle: 'danger',
    confirmLabel: t('main.dialog.delete'),
    rejectLabel: t('main.dialog.cancel'),
    rejectStyle: 'secondary'
  });

  if (!confirmed) return;

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

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

  await loadCwd();

  window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
  deleteInProgressDialog.value.close();
}

async function renameHandler(file, newName) {
  if (file.name === newName) return;

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

      if (!confirmed) return;

      await directoryModel.rename(directoryModel.buildFilePath(cwd.value, file.name), sanitize(cwd.value + '/' + newName), true /* overwrite */);
      await loadCwd();
    }
    else console.error(`Failed to rename ${file} to ${newName}`, e);
  }
}

async function changeOwnerHandler(files, newOwnerUid) {
  if (!files) return;

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

  await loadCwd();
}

async function pasteHandler(action, files, target) {
  if (!files || !files.length) return;

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

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

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

  await loadCwd();

  window.removeEventListener('beforeunload', beforeUnloadListener, { capture: true });
  pasteInProgressDialog.value.close();
}

async function downloadHandler(file) {
  await directoryModel.download(directoryModel.buildFilePath(cwd.value, file.name));
}

async function extractHandler(file) {
  extractInProgressDialog.value.open();
  await directoryModel.extract(directoryModel.buildFilePath(cwd.value, file.name));
  await loadCwd();
  extractInProgressDialog.value.close();
}

async function uploadHandler(targetDir, file, progressHandler) {
  uploadRequest.value = directoryModel.upload(targetDir, file, progressHandler);

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

  uploadRequest.value = null;

  await loadCwd();
}

async function loadCwd() {
  items.value = await directoryModel.listFiles(cwd.value);

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

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

  items.value.forEach(item => {
    if (item.type === 'directory') item.href = `#/home/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`;
    else item.href = `#/viewer/${resourceType.value}/${resourceId.value}${sanitize(cwd.value + '/' + item.name)}`;
  });

  busy.value = false;
}

async function onRestartApp() {
  if (resourceType.value !== 'app') return;

  const confirmed = await inputDialog.value.confirm({
    message: t('filemanager.toolbar.restartApp') + '?',
    confirmLabel: t('main.action.restart'),
    confirmStyle: 'danger',
    rejectLabel: t('main.dialog.cancel'),
    rejectStyle: 'secondary',
  });

  if (!confirmed) return;

  busyRestart.value = true;

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

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

  while(true) {
    let result;
    try {
      result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${resourceId.value}`, { access_token: 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);
  }

  busyRestart.value = false;
}

onBeforeRouteUpdate(async (to) => {
  if (to.params.type !== 'app' && to.params.type !== 'volume') return onFatalError(`Unknown type ${to.params.type}`);

  if ((to.params.type !== resourceType.value) || (to.params.resourceId !== resourceId.value)) {
    resourceType.value = to.params.type;
    resourceId.value = to.params.resourceId;

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

  cwd.value = to.params.cwd ? `/${to.params.cwd.join('/')}` : '/';

  await loadCwd();
});

onMounted(async () => {
  busy.value = true;

  const type = route.params.type || 'app';
  resourceId.value = route.params.resourceId;
  cwd.value = sanitize('/' + (route.params.cwd ? route.params.cwd.join('/') : '/'));

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

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

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

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

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

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

  resourceType.value = type;

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

  loadCwd();
});

</script>

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

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

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

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

      <Dialog ref="deleteInProgressDialog" :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>
          <a v-if="appLink" class="title" :href="appLink" target="_blank">{{ title }}</a>
          <span v-else class="title">{{ title }}</span>
        </template>
        <template #right>
          <ButtonGroup>
            <Button :menu="createMenuModel">{{ $t('filemanager.toolbar.new') }}</Button>
            <Button :menu="uploadMenuModel">{{ $t('filemanager.toolbar.upload') }}</Button>
          </ButtonGroup>

          <Button style="margin: 0 20px;" v-tooltip="$t('filemanager.toolbar.restartApp')" secondary tool :loading="busyRestart" icon="fa-solid fa-arrows-rotate" @click="onRestartApp" v-show="resourceType === 'app'"/>

          <ButtonGroup>
            <Button :href="'/terminal.html?id=' + resourceId + '&cwd=/app/data' + cwd" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-terminal" v-tooltip="$t('terminal.title')" />
            <Button :href="'/logs.html?appId=' + resourceId" target="_blank" v-show="resourceType === 'app'" secondary tool icon="fa-solid fa-align-left" v-tooltip="$t('logs.title')" />
          </ButtonGroup>
        </template>
      </TopBar>
    </template>
    <template #body>
      <div class="main-view">
        <div class="main-view-col tree-view-col">
          <TreeView
            v-if="!busy"
            :list-files="treeListFiles"
            :active-path="cwd"
            :fallback-icon="fallbackIcon"
            root-label="/app/data"
            :drop-handler="onTreeDrop"
            @navigate="onTreeNavigate"
          />
        </div>
        <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"
            :refresh-handler="onRefresh"
            :drop-handler="onDrop"
            :items="items"
            :owners-model="ownersModel"
            :fallback-icon="fallbackIcon"
            :tr="$t"
          />
        </div>
        <div class="main-view-col" style="max-width: 300px;">
          <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"
      />
    </template>
  </MainLayout>
</template>

<style scoped>

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

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

.tree-view-col {
  flex-grow: 0;
  flex-shrink: 0;
  width: 250px;
  border-right: 1px solid var(--pankow-input-border-color);
  overflow: auto;
}

.title {
  font-size: 20px;
  color: var(--pankow-color-text);
}

</style>

<style>

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

a.open-action {
  color: var(--pankow-color-text);
}

</style>
