import { getDataTransferFiles, renameFile } from './dom';

// Guard against pathological drops (deeply nested / huge trees) that could
// otherwise queue thousands of uploads and freeze the composer.
export const MAX_DROPPED_FILES = 500;

/**
 * Synchronously collect the `FileSystemEntry` objects for every item in a
 * drop's `DataTransfer`.
 *
 * This MUST be called synchronously inside the drop event handler: the
 * `DataTransferItemList` is emptied once the handler returns, so calling
 * `webkitGetAsEntry()` after an `await` yields `null`. Capture the entries
 * first, then traverse them asynchronously with {@link filesFromEntries}.
 *
 * Returns an empty array when `webkitGetAsEntry` is unavailable (non-Chromium
 * browsers), signalling the caller to fall back to the flat file list.
 */
export const entriesFromDataTransfer = (dataTransfer: DataTransfer): FileSystemEntry[] => {
  const entries: FileSystemEntry[] = [];
  const { items } = dataTransfer;
  if (!items) return entries;

  for (let i = 0; i < items.length; i += 1) {
    const item = items[i];
    if (item && item.kind === 'file' && typeof item.webkitGetAsEntry === 'function') {
      const entry = item.webkitGetAsEntry();
      if (entry) entries.push(entry);
    }
  }

  return entries;
};

const fileFromFileEntry = (entry: FileSystemFileEntry): Promise<File> =>
  new Promise((resolve, reject) => {
    entry.file(resolve, reject);
  });

/**
 * Read every entry from a directory reader.
 *
 * `readEntries` returns results in BATCHES (Chromium yields at most ~100 per
 * call), so it must be called repeatedly until it resolves with an empty array.
 */
const readAllDirectoryEntries = (reader: FileSystemDirectoryReader): Promise<FileSystemEntry[]> =>
  new Promise((resolve, reject) => {
    const all: FileSystemEntry[] = [];
    const readBatch = () => {
      reader.readEntries((batch) => {
        if (batch.length === 0) {
          resolve(all);
          return;
        }
        all.push(...batch);
        readBatch();
      }, reject);
    };
    readBatch();
  });

/**
 * Recursively walk `FileSystemEntry` objects (as produced by
 * {@link entriesFromDataTransfer}) and resolve them into a flat `File[]`,
 * descending into every nested directory.
 *
 * Nested files keep their relative folder path as a name prefix (e.g.
 * `photos/2024/pic.jpg`) so uploads remain distinguishable. Traversal stops
 * once `maxFiles` files have been collected.
 */
export const filesFromEntries = async (
  entries: FileSystemEntry[],
  maxFiles: number = MAX_DROPPED_FILES,
): Promise<File[]> => {
  const files: File[] = [];

  const walk = async (entry: FileSystemEntry, prefix: string): Promise<void> => {
    if (files.length >= maxFiles) return;

    // A single unreadable file/directory (moved between drop and read, a
    // permissions/lock error, an OS special file) must NOT abort the whole
    // traversal — skip it and keep collecting the rest.
    if (entry.isFile) {
      try {
        const file = await fileFromFileEntry(entry as FileSystemFileEntry);
        if (files.length >= maxFiles) return;
        files.push(prefix ? renameFile(file, `${prefix}${file.name}`) : file);
      } catch {
        /* skip unreadable file */
      }
      return;
    }

    if (entry.isDirectory) {
      let childEntries: FileSystemEntry[] = [];
      try {
        const reader = (entry as FileSystemDirectoryEntry).createReader();
        childEntries = await readAllDirectoryEntries(reader);
      } catch {
        return; /* skip unreadable directory */
      }
      const childPrefix = `${prefix}${entry.name}/`;
      for (const child of childEntries) {
        if (files.length >= maxFiles) break;
        // eslint-disable-next-line no-await-in-loop
        await walk(child, childPrefix);
      }
    }
  };

  for (const entry of entries) {
    if (files.length >= maxFiles) break;
    // eslint-disable-next-line no-await-in-loop
    await walk(entry, '');
  }

  return files;
};

/**
 * Extract dropped files, descending into any dropped folders.
 *
 * Captures the `FileSystemEntry` list synchronously (required — see
 * {@link entriesFromDataTransfer}) then traverses it asynchronously. Falls back
 * to the flat `dataTransfer.files` list when the directory API is unavailable
 * (non-Chromium) or when no entries are exposed.
 */
export const collectDroppedFiles = (dataTransfer: DataTransfer): Promise<File[] | undefined> => {
  const entries = entriesFromDataTransfer(dataTransfer);
  if (entries.length === 0) {
    return Promise.resolve(getDataTransferFiles(dataTransfer));
  }
  return filesFromEntries(entries).then((files) => (files.length > 0 ? files : undefined));
};
