/* eslint-disable no-continue */
/* eslint-disable class-methods-use-this */
/*
Copyright 2022 Pawel Psztyc
Licensed under the CC-BY 2.0
*/
import { html, TemplateResult, LitElement, CSSResult, PropertyValueMap } from 'lit';
import { property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import {
  ProjectKind, IFile, FolderKind, IFileCreateOptions, IUser,
  IApplication, DataFileKind, IPatchRevision, SerializableError, ContextListOptions, BroadcastEvent, FileMetaCreatedBroadcastEvent, FileDeletedBroadcastEvent, FilePatchBroadcastEvent, FileAccessBroadcastEvent,
} from '@api-client/core/build/browser.js';
import { Patch } from '@api-client/json';
import '@github/relative-time-element';
import '../src/define/ui/ui-icon.js';
import '../src/define/dialog/confirm-delete-dialog.js';
import { Events } from '../src/events/Events.js';
import { EventTypes } from '../src/events/EventTypes.js';
import styles from './ApiFilesElement.styles.js';
import layout from '../src/styles/global-styles.js';
import { IconsMap, DefaultNamesMap, AddLabelsMap } from './FileMaps.js';

interface SelectionInfo {
  key: string;
  kind: string;
}

const pageLimit = 100;

const AllKinds = [FolderKind, ProjectKind, DataFileKind];

/**
 * An element that renders a list of files from the API store.
 * It can be configured to render different types of files via the `kinds` property.
 * 
 * ## Usage
 * 
 * By default the store returns all files available to the user. To limit the types of files
 * allowed in the view set the `kinds` property.
 * 
 * ```html
 * <api-files .kinds="${["Core#Project"]}" .appInfo="${AppInfo}"></api-files>
 * ```
 * 
 * Additionally, the element does not initialize observing for files change. The hosting screen
 * must call the store to start watching for file changes. The hosting screen is also responsible 
 * for closing the files observer.
 * 
 * The element observers the files state change event dispatched by the store bindings.
 * This allows to have a synchronized with the store list of files.
 * 
 * @fires open
 * @fires viewstatechange
 */
export default class ApiFilesElement extends LitElement {
  static override get styles(): CSSResult[] {
    return [styles, layout];
  }

  /**
   * This property is required for the API access to work.
   * Set it to the current application information.
   * 
   * It will throw an error when trying to patch access without setting this property.
   */
  @property({ type: Object }) appInfo?: IApplication;

  /**
   * The list of file kinds to render in the view.
   * User spaces are always rendered.
   * @attribute
   */
  @property({ type: Array }) kinds?: string[];

  /**
   * Renders a button allowing to add a new file.
   * The list of options in the list depends on the `kinds` property.
   * 
   * Spaces are always included when this is enabled.
   * @attribute
   */
  @property({ type: Boolean }) allowAdd?: boolean;

  @property({ type: Boolean }) anypoint?: boolean;

  /**
   * The list title to render.
   * @default "Your files"
   * @attribute
   */
  @property({ type: String }) listTitle = 'Your files';

  @state() protected loadingFiles = false;

  @state() protected loadingFile = false;

  /**
   * The list type to render.
   * @default "grid"
   * @attribute
   */
  @property({ type: String }) viewType: 'grid' | 'list' = 'grid';

  /**
   * Allows to perform multi selection on the files list.
   */
  @property({ type: Boolean }) multiSelect = false;

  /**
   * Whether to allow to share selected file(s)
   * User may not be authorized to share a file.
   */
  @property({ type: Boolean }) canShare = false;

  /**
   * Whether to allow to trash selected file(s)
   * User may not be authorized to trash a file.
   */
  @property({ type: Boolean }) canTrash = false;

  /**
   * The type of view to render.
   * By default it renders the "owned" type which are the user files.
   * When set to shared it requests for files that has been shared with the user.
   */
  @property({ type: String }) type: 'owned' | 'shared' = 'owned';

  /**
   * The list of currently rendered files.
   */
  @state() protected files: IFile[] = [];

  /**
   * The page cursor for the files pagination.
   */
  protected filesCursor?: string;

  private _parent?: string;

  private _scrollTarget?: EventTarget;

  /**
   * Set on the element to add support for user scrolling through the list.
   * It is the reference to the parent element that is scrolling.
   * 
   * @default window
   */
  @property({ type: Object })
  get scrollTarget(): EventTarget | undefined {
    return this._scrollTarget;
  }

  set scrollTarget(value: EventTarget | undefined) {
    const old = this._scrollTarget;
    if (old === value) {
      return;
    }
    this._scrollTarget = value;
    if (old) {
      old.removeEventListener('scroll', this._scrollHandler);
    }
    if (value) {
      value.addEventListener('scroll', this._scrollHandler, { passive: true })
    }
  }

  /**
   * The currently rendered space key.
   * Setting the value will trigger reading files from the server.
   */
  @property({ type: String }) parent?: string;

  /**
   * A flag that prevents from requesting more files when the previous
   * file response returned no files.
   */
  private hasMoreFiles = true;

  /**
   * The file metadata for the `parent`
   */
  @state() protected file?: IFile;

  /**
   * The list of ids of currently selected files.
   */
  @state() protected selected: SelectionInfo[] = [];

  /**
   * Whether theres's a selection made in the file browser.
   */
  get hasSelection(): boolean {
    return this.selected.length > 0;
  }

  /**
   * The current user to pass to the share file component.
   */
  @property({ type: Object }) user?: IUser;

  constructor() {
    super();
    this._fileMetaHandler = this._fileMetaHandler.bind(this);
    this._scrollHandler = this._scrollHandler.bind(this);
    this.scrollTarget = window;
  }

  override connectedCallback(): void {
    super.connectedCallback();
    if (this.parent && !this.loadingFile) {
      this.loadFile(this.parent);
    }
    window.addEventListener(EventTypes.Store.File.State.change, this._fileMetaHandler);
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback();
    window.removeEventListener(EventTypes.Store.File.State.change, this._fileMetaHandler);
    this.scrollTarget = undefined;
  }

  protected override willUpdate(cp: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
    super.willUpdate(cp);
    if (cp.has('type') || cp.has('parent') || cp.has('kinds')) {
      this.cleanup();
      this.loadFiles();
    }
    if (cp.has('parent')) {
      this.file = undefined;
      if (this.parent) {
        this.loadFile(this.parent);
      }
    }
  }

  private cleanup(): void {
    this.files = [];
    this.filesCursor = undefined;
    this.file = undefined;
    this.hasMoreFiles = true;
    this.selected = [];
  }

  async loadFiles(): Promise<void> {
    if (!this.hasMoreFiles) {
      return;
    }
    const { type, parent, filesCursor } = this;
    const kinds = this.kinds as any[] | undefined;

    this.loadingFiles = true;
    const opts: ContextListOptions = {};
    if (filesCursor) {
      opts.cursor = filesCursor;
    } else {
      opts.limit = pageLimit;
      if (parent) {
        opts.parent = parent;
      }
    }
    const useShared = type === 'shared' && !parent;
    try {
      const result = useShared ?
        await Events.Store.File.listShared(kinds, opts) :
        await Events.Store.File.list(kinds, opts);
      if (result.items.length) {
        this.files = this.files.concat(result.items);
        if (result.items.length < pageLimit) {
          this.hasMoreFiles = false;
        }
      } else {
        this.hasMoreFiles = false;
      }
      this.filesCursor = result.cursor;
    } finally {
      this.loadingFiles = false;
    }
  }

  async loadFile(key: string): Promise<void> {
    this.loadingFile = true;
    try {
      const result = await Events.Store.File.read(key, false);
      this.file = result;
    } finally {
      this.loadingFile = false;
    }
  }

  protected _scrollHandler(e: Event): void {
    const target = e.target as HTMLElement;
    const { scrollTop, offsetHeight, scrollHeight } = target;
    const bottom = scrollTop + offsetHeight >= scrollHeight;
    if (bottom) {
      this.loadFiles();
    }
  }

  protected _fileMetaHandler(input: Event): void {
    const e = input as CustomEvent;
    const event = e.detail as BroadcastEvent;
    switch (event.operation) {
      case 'created': this._handleMetaCreated(event as FileMetaCreatedBroadcastEvent); break;
      case 'deleted': this._handleMetaDeleted(event as FileDeletedBroadcastEvent); break;
      case 'patch': this._handleMetaPatch(event as FilePatchBroadcastEvent); break;
      case 'access-granted': this._handleMetaAccessGranted(event as FileAccessBroadcastEvent); break;
      case 'access-removed': this._handleMetaAccessRemoved(event as FileAccessBroadcastEvent); break;
      default:

    }
  }

  protected _handleMetaCreated(event: FileMetaCreatedBroadcastEvent): void {
    const { parent, kind, data } = event;
    if (parent !== this.parent) {
      return;
    }
    if (![ProjectKind, FolderKind, DataFileKind].includes(kind)) {
      return;
    }
    this.files.push(data as IFile);
    this.requestUpdate();
  }

  protected _handleMetaDeleted(event: FileDeletedBroadcastEvent): void {
    const { kind, key } = event;
    if (![ProjectKind, FolderKind].includes(kind)) {
      return;
    }
    const index = this.files.findIndex(i => i.key === key);
    if (index >= 0) {
      this.files.splice(index, 1);
      this.requestUpdate();
    }
    if (key === this.parent) {
      this.parent = undefined;
    }
  }

  protected _handleMetaPatch(event: FilePatchBroadcastEvent): void {
    const { kind, key } = event;
    if (![ProjectKind, FolderKind].includes(kind)) {
      return;
    }
    const index = this.files.findIndex(i => i.key === key);
    if (index < 0) {
      return;
    }
    const rev = event.data as IPatchRevision;
    const { patch } = rev;
    const file = this.files[index];
    const result = Patch.apply(file, patch);
    this.files[index] = result.doc as IFile;
    this.requestUpdate();
  }

  protected async _handleMetaAccessGranted(event: FileAccessBroadcastEvent): Promise<void> {
    const { kind, key, parent } = event;
    if (parent !== this.parent) {
      return;
    }
    if (![ProjectKind, FolderKind].includes(kind)) {
      return;
    }
    const has = this.files.find(i => i.key === key);
    if (has) {
      return;
    }
    const file = await Events.Store.File.read(key as string, false);
    // prohibits adding a shared file to the root of user files.
    const hasParents = !!file.parents && !!file.parents.length;
    if (hasParents) {
      return;
    }
    if (!hasParents && this.type === 'owned') {
      if (!this.user || file.owner !== this.user?.key) {
        // either the user is not an owner or we can't determine that.
        return;
      }
    }
    this.files.push(file);
    this.requestUpdate();
  }

  protected async _handleMetaAccessRemoved(event: FileAccessBroadcastEvent): Promise<void> {
    const { kind, key, parent } = event;
    if (parent !== this.parent) {
      return;
    }
    if (![ProjectKind, FolderKind].includes(kind)) {
      return;
    }
    const index = this.files.findIndex(i => i.key === key);
    if (index >= 0) {
      this.files.splice(index, 1);
      this.requestUpdate();
    }
  }

  protected _addMenuHandler(e: Event): void {
    const list = e.target as any; // AnypointListboxElement;
    const dialog = document.createElement('add-file-dialog');
    // dialog.kind = String(list.selected);
    // dialog.opened = true;
    document.body.appendChild(dialog);
    list.selected = undefined;

    // dialog.addEventListener('closed', (ev: Event) => {
    //   const event = ev as CustomEvent;
    //   const { canceled, confirmed, value } = event.detail;
    //   if (!canceled && confirmed && value) {
    //     this.createFile(value, dialog.kind!);
    //   }
    //   document.body.removeChild(dialog);
    // });
  }

  protected async createFile(name: string, kind: string): Promise<void> {
    const { parent } = this;
    const opts: IFileCreateOptions = {};
    if (parent) {
      opts.parent = parent;
    }
    await Events.Store.File.createDefault(name, kind, opts);
  }

  protected _removeFile(id: string): void {
    const filesIndex = this.files.findIndex(i => i.key === id);
    if (filesIndex < 0) {
      return;
    }
    this.files.splice(filesIndex, 1);
    const selectedIndex = this.selected.findIndex(i => i.key === id);
    if (selectedIndex >= 0) {
      this.selected.splice(selectedIndex, 1);
    }

    this.requestUpdate();
  }

  protected _toggleViewTypeHandler(): void {
    this.viewType = this.viewType === 'grid' ? 'list' : 'grid';
    this.dispatchEvent(new Event('viewstatechange'));
  }

  protected _filesKeydown(e: KeyboardEvent): void {
    if (e.key !== 'Enter') {
      return;
    }
    const node = e.target as HTMLElement;
    const item = this._getListTarget(node);
    if (!item) {
      return;
    }
    const id = item.dataset.key as string;
    this.notifyOpen(id);
  }

  /**
   * This handles helps to manage the selection state of the files.
   */
  protected _gridClickHandler(e: MouseEvent): void {
    const node = e.target as HTMLElement;
    const item = this._getListTarget(node);
    if (!item) {
      return;
    }

    const { ctrlKey, shiftKey, metaKey } = e;
    const { multiSelect = false } = this;
    const bulkSelection = multiSelect && shiftKey;
    const fineSelection = multiSelect && !bulkSelection && (ctrlKey || metaKey);
    const isSelection = multiSelect && bulkSelection || fineSelection;
    
    if (!isSelection && this.selected.length) {
      this.selected = [];
    }

    const id = item.dataset.key as string;
    const kind = item.dataset.kind as string;
    const info: SelectionInfo = {
      key: id,
      kind,
    };

    if (!isSelection || fineSelection) {
      this._toggleSelectedFile(info);
      return;
    }
    // the user is selecting files in bulk
    const last = this.selected[this.selected.length - 1];
    if (!last) {
      this._toggleSelectedFile(info);
      return;
    }
    if (last.key === info.key) {
      this._toggleSelectedFile(info);
      return;
    }
    this._selectGridSelectionRange(last, info)
  }

  protected _tableClickHandler(e: MouseEvent): void {
    const node = e.target as HTMLElement;
    const item = this._getListTarget(node);
    if (!item) {
      return;
    }
    const { ctrlKey, shiftKey, metaKey } = e;
    const { multiSelect = false } = this;
    const bulkSelection = multiSelect && shiftKey;
    const fineSelection = multiSelect && !bulkSelection && (ctrlKey || metaKey);
    const isSelection = multiSelect && bulkSelection || fineSelection;
    if (!isSelection && this.selected.length) {
      this.selected = [];
    }
    const id = item.dataset.key as string;
    const kind = item.dataset.kind as string;
    const info: SelectionInfo = {
      key: id,
      kind,
    };

    if (!isSelection || fineSelection) {
      this._toggleSelectedFile(info);
      return;
    }
    // the user is selecting files in bulk
    const last = this.selected[this.selected.length - 1];
    if (!last) {
      this._toggleSelectedFile(info);
      return;
    }
    if (last.key === info.key) {
      this._toggleSelectedFile(info);
      return;
    }
    this._selectTableSelectionRange(last, info);
  }

  protected _getListTarget(node: HTMLElement): HTMLElement | undefined {
    let current = node;
    while (current) {
      if (current.nodeType !== Node.ELEMENT_NODE) {
        current = current.parentElement as HTMLElement;
        continue;
      }
      if (current.dataset.key) {
        break;
      }
      current = current.parentElement as HTMLElement;
    }
    return current;
  }

  protected _toggleSelectedFile(info: SelectionInfo): void {
    const index = this.selected.findIndex(i => i.key === info.key);
    if (index >= 0) {
      this.selected.splice(index, 1);
    } else {
      this.selected.push(info);
    }
    this.requestUpdate();
  }

  /**
   * Selects all files between the `from` and `to`.
   * This detects whether selection should occur between the two grids.
   * 
   * @param from The from selection
   * @param to The to selection.
   */
  protected _selectGridSelectionRange(from: SelectionInfo, to: SelectionInfo): void {
    const fromElement = this.shadowRoot!.querySelector(`[data-key="${from.key}"]`) as HTMLElement;
    const toElement = this.shadowRoot!.querySelector(`[data-key="${to.key}"]`) as HTMLElement;
    const fromGrid = fromElement.parentElement as HTMLElement;
    const toGrid = toElement.parentElement as HTMLElement;
    const fromChildren = Array.from(fromGrid.children);
    const toChildren = Array.from(toGrid.children);
    const fromIndex = fromChildren.indexOf(fromElement);
    const toIndex = toChildren.indexOf(toElement);
    let children: Element[] = [];
    // the user selects files and folders
    if (from.kind !== to.kind) {
      if (from.kind === FolderKind) {
        // the user selects from a space to a file
        const spaceItems = fromChildren.slice(fromIndex + 1);
        const fileItems = toChildren.slice(0, toIndex + 1);
        children = spaceItems.concat(fileItems);
      } else {
        // the user selects from a file to a space
        const fileItems = fromChildren.slice(0, fromIndex);
        const spaceItems = toChildren.slice(toIndex);
        children = spaceItems.concat(fileItems);
      }
    } else {
      // users selects either files or folders
      const toRight = fromIndex < toIndex;
      // Node, Array.splice's end is not included
      const startIndex = toRight ? fromIndex + 1 : toIndex;
      const endIndex = toRight ? toIndex + 1 : fromIndex;
      children = fromChildren.slice(startIndex, endIndex);
    }
    this._selectChildren(children);
  }

  protected _selectTableSelectionRange(from: SelectionInfo, to: SelectionInfo): void {
    const fromElement = this.shadowRoot!.querySelector(`[data-key="${from.key}"]`) as HTMLElement;
    const toElement = this.shadowRoot!.querySelector(`[data-key="${to.key}"]`) as HTMLElement;
    const table = this.shadowRoot!.querySelector('table')!;
    const children = Array.from(table.querySelectorAll('tbody tr'));
    const fromIndex = children.indexOf(fromElement);
    const toIndex = children.indexOf(toElement);
    const toBottom = fromIndex < toIndex;
    // Node, Array.splice's end is not included
    const startIndex = toBottom ? fromIndex + 1 : toIndex;
    const endIndex = toBottom ? toIndex + 1 : fromIndex;
    const items = children.slice(startIndex, endIndex);
    this._selectChildren(items);
  }

  protected _selectChildren(items: Element[]): void {
    const { selected } = this;
    items.forEach((child) => {
      const elm = child as HTMLElement;
      const id = elm.dataset.key as string;
      const kind = elm.dataset.kind as string;
      if (selected.some(i => i.key === id)) {
        return;
      }
      selected.push({ key: id, kind });
    });
    this.requestUpdate();
  }

  protected _gridDblHandler(e: MouseEvent): void {
    const node = e.target as HTMLElement;
    const item = this._getListTarget(node);
    if (!item) {
      return;
    }
    const id = item.dataset.key as string;
    this.notifyOpen(id);
  }

  /**
   * Notifies the parent that the user requested the "open" action.
   * 
   * @param key The key of the file
   */
  protected notifyOpen(key: string): void {
    const file = this.files.find(i => i.key === key);
    if (!file) {
      return;
    }
    this.dispatchEvent(new CustomEvent('open', {
      detail: file,
    }));
  }

  protected _shareHandler(): void {
    const [item] = this.selected;
    const file = this.files.find(i => i.key === item.key);
    if (!file) {
      return;
    }
    const dialog = document.createElement('share-file');
    // dialog.file = file;
    // dialog.user = this.user;
    // dialog.appInfo = this.appInfo;
    // dialog.modal = true;
    // dialog.open = true;
    document.body.appendChild(dialog);
    dialog.addEventListener('closed', () => {
      document.body.removeChild(dialog);
    });
  }

  protected _deleteSelected(): void {
    const { selected } = this;
    const keys = selected.map(i => i.key);
    const files = this.files.filter(i => keys.includes(i.key));
    if (!files) {
      return;
    }
    const dialog = document.createElement('confirm-delete-dialog');
    dialog.open = true;
    dialog.type = 'files';
    dialog.names = files.map(i => i.info.name || 'Unnamed file');
    document.body.appendChild(dialog);
    dialog.addEventListener('closed', (ev: Event) => {
      document.body.removeChild(dialog);
      const event = ev as CustomEvent;
      const { canceled, confirmed } = event.detail;
      if (!canceled && confirmed) {
        this._deleteFiles(files.map(i => i.key));
      }
    });
  }

  protected async _deleteFiles(ids: string[]): Promise<void> {
    const ps = ids.map(id => Events.Store.File.delete(id));
    const results = await Promise.allSettled(ps);

    const messages: string[] = [];
    results.forEach((result) => {
      if (result.status === 'rejected') {
        const err = result.reason as SerializableError;
        messages.push(err.message);
      }
    });
    if (messages.length) {
      throw new Error(messages.join(`\n`));
    }
  }

  override render(): TemplateResult {
    const { listTitle, loadingFiles } = this;
    return html`
    ${loadingFiles ? html`<anypoint-progress indeterminate></anypoint-progress>` : ''}
    <div class="title-area">
      <h2 class="section-title text-selectable">${listTitle}</h2>
      <div class="right file-options">
        ${this.selectedToolbarTemplate()}
        ${this.spaceBaseControls()}
      </div>
    </div>
    ${this.filesTemplate()}
    `;
  }

  protected spaceBaseControls(): TemplateResult {
    const { viewType } = this;
    return html`
    ${this.addFileButtonTemplate()}
    <anypoint-icon-button title="Toggles between list and grid view" aria-label="Selected view is ${viewType}"
      @click="${this._toggleViewTypeHandler}">
      <ui-icon icon="${viewType === 'grid' ? 'viewGrid' : 'viewList'}"></ui-icon>
    </anypoint-icon-button>
    `;
  }

  protected addFileButtonTemplate(): TemplateResult | string {
    if (!this.allowAdd) {
      return '';
    }
    const { anypoint, kinds = AllKinds } = this;
    const list = [...kinds];
    if (!list.includes(FolderKind)) {
      list.unshift(FolderKind);
    }
    return html`
    <anypoint-menu-button dynamicAlign ?anypoint="${anypoint}">
      <anypoint-icon-button slot="dropdown-trigger" aria-label="Press to select a type of file to add"
        title="Add a new file or space. Press to select the type." ?anypoint="${anypoint}">
        <ui-icon icon="add"></ui-icon>
      </anypoint-icon-button>
      <anypoint-listbox class="dropdown-list-container" slot="dropdown-content" ?anypoint="${anypoint}"
        @select="${this._addMenuHandler}" attrForSelected="data-value">
        ${list.map(kind => this._addFileOptionTemplate(kind))}
      </anypoint-listbox>
    </anypoint-menu-button>
    `;
  }

  protected _addFileOptionTemplate(kind: string): TemplateResult {
    const icon = IconsMap[kind];
    const label = AddLabelsMap[kind];
    return html`
    <anypoint-icon-item ?anypoint="${this.anypoint}" data-value="${kind}" class="dropdown-option">
      <ui-icon icon="${icon}" slot="item-icon"></ui-icon>${label}
    </anypoint-icon-item>
    `;
  }

  protected filesTemplate(): TemplateResult {
    const { files, viewType } = this;
    if (!files || !files.length) {
      return html`
      <p class="empty-info">
        You have no files.
      </p>
      ${this.spacesIntroduction()}
      `;
    }
    const spaces = files.filter(i => i.kind === FolderKind);
    const metaFiles = files.filter(i => i.kind !== FolderKind);
    if (viewType === 'grid') {
      return this.filesGridTemplate(spaces, metaFiles);
    }
    return this.fileListTemplate(spaces, metaFiles);
  }

  protected filesGridTemplate(spaces: IFile[], files: IFile[]): TemplateResult {
    return html`
    <section class="spaces-grid" @click="${this._gridClickHandler}" @dblclick="${this._gridDblHandler}"
      @keydown="${this._filesKeydown}">
      ${spaces.map((item) => this.spaceTileTemplate(item))}
    </section>
    <div class="files-section-title">Files</div>
    <section class="files-grid" @click="${this._gridClickHandler}" @dblclick="${this._gridDblHandler}"
      @keydown="${this._filesKeydown}">
      ${files.map((item) => this.fileTileTemplate(item))}
    </section>
    `;
  }

  protected fileListTemplate(spaces: IFile[], files: IFile[]): TemplateResult {
    return html`
    <table class="files-list" @click="${this._tableClickHandler}" @dblclick="${this._gridDblHandler}"
      @keydown="${this._filesKeydown}">
      <thead>
        <tr>
          <th class="name-column">Name</th>
          <th class="owner-column">Owner</th>
          <th class="updated-column">Updated</th>
        </tr>
      </thead>
      <tbody>
        ${spaces.map((item) => this.fileListItemTemplate(item))}
        ${files.map((item) => this.fileListItemTemplate(item))}
      </tbody>
    </table>
    `;
  }

  protected fileListItemTemplate(file: IFile): TemplateResult {
    const { selected } = this;
    const { info, key, kind } = file;
    const title = info.name || DefaultNamesMap[kind];
    const icon = IconsMap[kind];
    const classes = {
      'selected-item': selected.some(i => i.key === key),
      'file-list-item': true,
    };
    const time = new Date(file.lastModified.time);
    return html`
    <tr class="${classMap(classes)}" data-key="${key}" data-kind="${kind}" tabindex="0">
      <td class="name-column" aria-label="${AddLabelsMap[kind] || 'File'}: ${title},">
        <ui-icon icon="${icon}" class="icon"></ui-icon>
        <div class="file-name">${title}</div>
      </td>
      <td class="owner-column" aria-label="Updated by: me,">me</td>
      <td class="updated-column">
        <relative-time datetime="${time.toISOString()}"></relative-time>
      </td>
    </tr>
    `;
  }

  protected spaceTileTemplate(item: IFile): TemplateResult {
    const { selected, type } = this;
    const { info, key, kind } = item;
    const title = info.name || DefaultNamesMap[kind];
    const classes = {
      'selected-item': selected.some(i => i.key === key),
      'space-tile': true,
    };
    const isShared = type === 'shared';
    return html`
    <div class="${classMap(classes)}" tabindex="0" data-key="${key}" data-kind="${kind}">
      <div class="space-icon">
        <ui-icon icon="${isShared ? 'folderShared' : 'folder'}" class="icon"></ui-icon>
      </div>
      <div class="space-label" aria-label="${AddLabelsMap[kind] || 'Folder'}: ${title}">${title}</div>
    </div>
    `;
  }

  protected fileTileTemplate(item: IFile): TemplateResult {
    const { selected } = this;
    const { info, key, kind } = item;
    const title = info.name || DefaultNamesMap[kind];
    const icon = IconsMap[kind];
    const classes = {
      'selected-item': selected.some(i => i.key === key),
      'file-tile': true,
    };
    return html`
    <div class="${classMap(classes)}" tabindex="0" data-key="${key}" data-kind="${item.kind}">
      <div class="file-thumb">
        <ui-icon icon="${icon}" class="icon"></ui-icon>
      </div>
      <div class="tile-label">
        <ui-icon icon="${icon}" class="icon"></ui-icon>
        <span class="file-label" aria-label="${AddLabelsMap[kind] || 'File'}: ${title}" title="${title}">${title}</span>
      </div>
    </div>
    `;
  }

  protected spacesIntroduction(): TemplateResult {
    return html`
    <div class="introduction">
      <b>Spaces</b> allow you to organize your work. When using a network store a space can be shared with
      other users.
    </div>
    `;
  }

  protected selectedToolbarTemplate(): TemplateResult | string {
    if (!this.hasSelection) {
      return '';
    }
    
    return html`
    ${this.shareButtonTemplate()}
    ${this.trashButtonTemplate()}
    <anypoint-icon-button title="File options">
      <ui-icon icon="moreVert"></ui-icon>
    </anypoint-icon-button>
    <span class="separator"></span>
    `;
  }

  protected shareButtonTemplate(): TemplateResult | string {
    if (!this.canShare) {
      return '';
    }
    // For simplicity, for now, we allow only one file share at the time.
    let canShare = this.selected.length === 1;
    if (canShare) {
      const [id] = this.selected;
      const file = this.files.find(i => i.key === id.key) as IFile;
      if (!file) {
        canShare = false;
      } else if (file.capabilities) {
        canShare = file.capabilities.canShare;
      } else {
        canShare = false;
      }
    }
    return html`
    <anypoint-icon-button title="Share file" ?disabled="${!canShare}" @click="${this._shareHandler}">
      <ui-icon icon="personAdd"></ui-icon>
    </anypoint-icon-button>
    `;
  }

  protected trashButtonTemplate(): TemplateResult | string {
    if (!this.canTrash) {
      return '';
    }
    const { selected, files } = this;
    const allowed = !selected.some((selectionInfo) => {
      // find one that cannot be shared.
      const file = files.find(i => i.key === selectionInfo.key);
      if (!file) {
        // invalid selection, apparently.
        return true;
      }
      if (!file.capabilities) {
        // invalid file definition
        return true;
      }
      return !file.capabilities.canDelete;
    });

    return html`
    <anypoint-icon-button title="Move file to thrash" ?disabled="${!allowed}" @click="${this._deleteSelected}">
      <ui-icon icon="deleteFile"></ui-icon>
    </anypoint-icon-button>
    `;
  }
}
