import { 
  BroadcastEvent,
  BroadcastFileData,
  ContextListResult,
  ContextSpaceListOptions,
  DeletedBroadcastEvent,
  FileAccessBroadcastEvent,
  FileBreadcrumb, 
  FileMetaCreatedBroadcastEvent, 
  FilePatchBroadcastEvent, 
  IFile,
  ListFileKind,
} from "@api-client/core/build/browser.js";
import { Patch } from "@api-client/json";
import { filesSortFunction } from "../elements/store/FilesLib.js";
import { Events } from "../events/Events.js";
import { StoreBroadcast } from "../http-client/store/StoreBroadcast.js";

type FilesSource = 'own' | 'shared';

/**
 * Contains a logic to read files from the store.
 * 
 * It uses app events to query for the data.
 */
export class FileSystem extends EventTarget {
  source: FilesSource = 'own';

  /**
   * The list of files to render.
   * Do not overwrite this property or when you must then update the DataTable's `items` as well.
   */
  files: IFile[] = [];

  /**
   * The pagination cursor for files.
   */
  cursor?: string;

  /**
   * This is set when the last files query did not yeld results.
   */
  queryEnded?: boolean;

  /**
   * The currently rendered breadcrumbs.
   */
  breadcrumbs?: FileBreadcrumb[];

  /**
   * Whether the UI is reading files 
   */
  reading?: boolean;

  /**
   * The list of file kinds to list.
   * Folders are always included.
   */
  kinds?: ListFileKind[];

  /**
   * The timeout for the query debouncer.
   * When any property change this is the time the element will wait
   * until the actual query is made.
   */
  debounceTimeout = 100;

  debouncerValue?: number;

  /**
   * The variable parent folder key.
   * This is set when interacting with the UI or when the `folder` attribute change.
   */
  parent?: string;

  /**
   * The page limit. Defaults to the store defaults.
   * @attribute
   */
  limit?: number;

  filesChannel = new BroadcastChannel(StoreBroadcast.files);

  eventsTarget: EventTarget = document.body;

  constructor() {
    super();
    this.fileMetaMessageHandler = this.fileMetaMessageHandler.bind(this);
  }

  observe(): void {
    this.filesChannel.addEventListener('message', this.fileMetaMessageHandler);
  }

  unobserve(): void {
    this.filesChannel.removeEventListener('message', this.fileMetaMessageHandler);
  }

  fileMetaMessageHandler(e: MessageEvent): void {
    const event = e.data as BroadcastEvent & BroadcastFileData;
    if (event.parent !== this.parent) {
      return;
    }
    if (event.alt !== 'meta') {
      return;
    }
    switch (event.operation) {
      case 'created': this.handleFileCreated((event as FileMetaCreatedBroadcastEvent).data); break;
      case 'deleted': this.handleFileDeleted(event as DeletedBroadcastEvent); break;
      case 'patch': this.handleFilePatch(event as FilePatchBroadcastEvent); break;
      case 'access-granted': this.handleFileAccessGranted(event as FileAccessBroadcastEvent); break;
      case 'access-removed': this.handleFileAccessRemoved(event as FileAccessBroadcastEvent); break;
      default:
    }
  }

  handleFileCreated(file: IFile): void {
    this.files.push(file);
    // We don't sort here so files that appear at the end which makes sense because the list is scrolling.
    // this.files.sort(filesSortFunction);
    this.notifyUpdate();
  }

  handleFileDeleted(event: DeletedBroadcastEvent): void {
    this.removeFileFromList(event.key);
  }

  async handleFileAccessGranted(event: FileAccessBroadcastEvent): Promise<void> {
    if (this.source !== 'shared') {
      return;
    }
    const { key } = event;
    try {
      const file = await Events.Store.File.read(key, false, this.eventsTarget);
      const index = this.files.findIndex(i => i.key === key);
      if (index >= 0) {
        this.files.splice(index, 1);
      }
      this.handleFileCreated(file);
    } catch (e) {
      // eslint-disable-next-line no-console
      console.warn(e);
    }
  }

  handleFileAccessRemoved(event: FileAccessBroadcastEvent): void {
    if (this.source !== 'shared') {
      return;
    }
    const { key } = event;
    this.removeFileFromList(key);
  }

  removeFileFromList(key: string): void {
    const index = this.files.findIndex(i => i.key === key);
    if (index === -1) {
      return;
    }
    this.files.splice(index, 1);
    this.notifyUpdate();
    this.dispatchEvent(new CustomEvent('delete', { detail: key }));
  }

  handleFilePatch(event: FilePatchBroadcastEvent): void {
    const { data, key } = event;
    const index = this.files.findIndex(i => i.key === key);
    if (index === -1) {
      return;
    }
    const file = this.files[index];
    const result = Patch.apply(file, data.patch);
    this.files[index] = result.doc as IFile;
    this.notifyUpdate();
  }

  debounceQuery(): void {
    if (this.debouncerValue) {
      clearTimeout(this.debouncerValue);
    }
    this.debouncerValue = window.setTimeout(() => { 
      this.queryPage();
      this.queryBreadcrumbs();
    }, this.debounceTimeout);
  }

  resetList(): void {
    this.files = [];
    this.cursor = undefined;
    this.breadcrumbs = undefined;
    this.queryEnded = false;
  }

  async queryPage(): Promise<void> {
    if (this.reading || this.queryEnded) {
      return;
    }
    this.reading = true;
    this.notifyUpdate();
    try {
      const result = await this.listFiles();
      if (!result) {
        throw new Error(`Files event not handled.`);
      }
      const { items, cursor } = result;
      if (!items.length) {
        this.queryEnded = true;
      } else {
        if (!this.files.length) {
          // we only sort the first page as later the user scrolls and sorting would change the order.
          items.sort(filesSortFunction);
        }
        this.files = this.files.concat(items);
      }
      if (cursor) {
        this.cursor = cursor;
      }
    } catch(e) {
      const err = e as Error;
      this.notifyError(`Unable to load files list. ${err.message}`);
      // eslint-disable-next-line no-console
      console.error(e);
    } finally {
      this.reading = false;
    }
    this.dispatchEvent(new Event('querycomplete'));
    this.notifyUpdate();
  }

  async queryBreadcrumbs(): Promise<void> {
    if (this.parent) {
      const br = await this.listBreadcrumbs(this.parent);
      this.breadcrumbs = br.items;
    } else {
      this.breadcrumbs = undefined;
    }
  }

  getPageQueryOptions(): ContextSpaceListOptions {
    const { cursor: filesCursor, parent, limit } = this;
    const opts: ContextSpaceListOptions = {
      space: '', // this is filled by the bindings.
      descending: true,
    };
    if (filesCursor) {
      opts.cursor = filesCursor;
    }
    if (parent) {
      opts.parent = parent;
    }
    if (typeof limit === 'number' && limit) {
      opts.limit = limit;
    }
    return opts;
  }

  async listFiles(): Promise<ContextListResult<IFile>> {
    const { kinds } = this;
    const opts = this.getPageQueryOptions();
    if (this.source === 'shared') {
      return Events.Store.File.listShared(kinds, opts, this.eventsTarget);
    }
    return Events.Store.File.list(kinds, opts, this.eventsTarget);
  }

  async listBreadcrumbs(folder: string): Promise<ContextListResult<FileBreadcrumb>> {
    return Events.Store.File.breadcrumbs(folder, this.eventsTarget);
  }

  notifyError(error: string): void {
    this.dispatchEvent(new CustomEvent<string>('error', {
      detail: error,
    }));
  }

  notifyUpdate(): void {
    this.dispatchEvent(new Event('change'));
  }

  /**
   * Sets a parent and makes the query.
   * @param key The key of the parent to query. Empty for root folder.
   */
  selectFolder(key?: string): void {
    this.parent = key;
    this.resetList();
    this.debounceQuery();
    // this.queryPage();
    this.queryBreadcrumbs();
  }

  /**
   * Moves to a parent folder, if any.
   */
  async parentUp(): Promise<void> {
    const { breadcrumbs = [] } = this;
    const [, parent] = breadcrumbs;
    if (!parent) {
      if (!this.parent) {
        return;
      }
      this.parent = undefined;
    } else {
      if (this.parent === parent.key) {
        return;
      }
      this.parent = parent.key;
    }
    this.resetList();
    await Promise.all([
      this.queryPage(),
      this.queryBreadcrumbs(),
    ]);
    this.notifyUpdate();
  }

  /**
   * @param target The HTML element that is a scrollable list if files.
   * @returns True when the scroll position indicates that the user scrolled near the end of the list.
   */
  isListEnd(target: HTMLElement): boolean {
    const { scrollTop, offsetHeight, scrollHeight } = target;
    // 20 is the offset which qualifies as the end. An arbitrary number.
    return scrollTop + offsetHeight >= scrollHeight - 20; 
  }
}
