// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../../ui/legacy/components/data_grid/data_grid.js';

import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import type {PlatformFileSystem} from '../../models/persistence/PlatformFileSystem.js';
import * as UI from '../../ui/legacy/legacy.js';
import {Directives, html, render} from '../../ui/lit/lit.js';

import editFileSystemViewStyles from './editFileSystemView.css.js';

const {styleMap} = Directives;

const UIStrings = {
  /**
   * @description Text in Edit File System View of the Workspace settings in Settings to indicate that the following string is a folder URL
   */
  url: 'URL',
  /**
   * @description Text in Edit File System View of the Workspace settings in Settings
   */
  excludedFolders: 'Excluded sub-folders',
  /**
   * @description Error message when a file system path is an empty string.
   */
  enterAPath: 'Enter a path',
  /**
   * @description Error message when a file system path is identical to an existing path.
   */
  enterAUniquePath: 'Enter a unique path',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/settings/EditFileSystemView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export const enum ExcludedFolderStatus {
  VALID = 1,
  ERROR_NOT_A_PATH = 2,
  ERROR_NOT_UNIQUE = 3,
}

function statusString(status: ExcludedFolderStatus): Platform.UIString.LocalizedString {
  switch (status) {
    case ExcludedFolderStatus.ERROR_NOT_A_PATH:
      return i18nString(UIStrings.enterAPath);
    case ExcludedFolderStatus.ERROR_NOT_UNIQUE:
      return i18nString(UIStrings.enterAUniquePath);
    case ExcludedFolderStatus.VALID:
      throw new Error('unreachable');
  }
}

export interface PathWithStatus {
  path: Platform.DevToolsPath.EncodedPathString;
  status: ExcludedFolderStatus;
}

export interface EditFileSystemViewInput {
  fileSystemPath: Platform.DevToolsPath.UrlString;
  excludedFolderPaths: PathWithStatus[];
  onCreate: (event: CustomEvent<{url?: string}>) => void;
  onEdit: (event: CustomEvent<{columnId: string, valueBeforeEditing: string, newText: string}>) => void;
  onDelete: (event: CustomEvent) => void;
}

export type View = (input: EditFileSystemViewInput, output: object, target: HTMLElement) => void;

export const DEFAULT_VIEW: View = (input, _output, target) => {
  // clang-format off
  render(html`
      <style>${editFileSystemViewStyles}</style>
      <div class="excluded-folder-header">
        <span>${i18nString(UIStrings.url)}</span>
        <span class="excluded-folder-url">${input.fileSystemPath}</span>
        <devtools-data-grid
          @create=${input.onCreate}
          class="exclude-subfolders-table"
          parts="excluded-folder-row-with-error"
          inline striped>
          <table>
            <thead>
              <tr>
                <th id="url" editable>${i18nString(UIStrings.excludedFolders)}</th>
              </tr>
            </thead>
            <tbody>
            ${input.excludedFolderPaths.map((path, index) => html`
              <tr data-url=${path.path} data-index=${index}
                  @edit=${input.onEdit}
                  @delete=${input.onDelete}>
                <td style=${styleMap({backgroundColor: path.status !== ExcludedFolderStatus.VALID ? 'var(--sys-color-error-container)' : undefined})}>${path.path}</td>
              </tr>
            `)}
            <tr placeholder></tr>
            </tbody>
          </table>
        </devtools-data-grid>
        ${input.excludedFolderPaths.filter(({status}) => status !== ExcludedFolderStatus.VALID).map(({status}) =>
          html`<span class="excluded-folder-error">${statusString(status)}</span>`)}
    </div>`, target);
  // clang-format on
};

export class EditFileSystemView extends UI.Widget.VBox {
  #fileSystem?: PlatformFileSystem;
  #excludedFolderPaths: PathWithStatus[] = [];
  readonly #view: View;

  constructor(element: HTMLElement|undefined, view: View = DEFAULT_VIEW) {
    super(element);
    this.#view = view;
  }

  set fileSystem(fileSystem: PlatformFileSystem) {
    this.#fileSystem = fileSystem;
    this.#resyncExcludedFolderPaths();
    this.requestUpdate();
  }

  override wasShown(): void {
    super.wasShown();
    this.#resyncExcludedFolderPaths();
    this.requestUpdate();
  }

  #resyncExcludedFolderPaths(): void {
    this.#excludedFolderPaths = this.#fileSystem?.excludedFolders()
                                    .values()
                                    .map(path => ({path, status: ExcludedFolderStatus.VALID}))
                                    .toArray() ??
        [];
  }

  override performUpdate(): void {
    const input: EditFileSystemViewInput = {
      fileSystemPath: this.#fileSystem?.path() ?? Platform.DevToolsPath.urlString``,
      excludedFolderPaths: this.#excludedFolderPaths,
      onCreate: e => this.#onCreate(e.detail.url),
      onEdit: e => this.#onEdit(
          (e.currentTarget as HTMLElement).dataset.index ?? '-1', e.detail.valueBeforeEditing, e.detail.newText),
      onDelete: e => this.#onDelete((e.currentTarget as HTMLElement).dataset.index ?? '-1'),
    };
    this.#view(input, {}, this.contentElement);
  }

  #onCreate(url?: string): void {
    if (url === undefined) {
      // The data grid fires onCreate even when the user just selects and then deselects the
      // creation row. Ignore those occurrences.
      return;
    }

    const pathWithStatus = this.#validateFolder(url);
    this.#excludedFolderPaths.push(pathWithStatus);
    if (pathWithStatus.status === ExcludedFolderStatus.VALID) {
      this.#fileSystem?.addExcludedFolder(pathWithStatus.path);
    }

    this.requestUpdate();
  }

  #onEdit(idx: string, valueBeforeEditing: string, newText: string): void {
    const index = Number.parseInt(idx, 10);
    if (index < 0 || index >= this.#excludedFolderPaths.length) {
      return;
    }

    const pathWithStatus = this.#validateFolder(newText);
    const oldPathWithStatus = this.#excludedFolderPaths[index];
    this.#excludedFolderPaths[index] = pathWithStatus;

    if (oldPathWithStatus.status === ExcludedFolderStatus.VALID) {
      this.#fileSystem?.removeExcludedFolder(valueBeforeEditing as Platform.DevToolsPath.EncodedPathString);
    }

    if (pathWithStatus.status === ExcludedFolderStatus.VALID) {
      this.#fileSystem?.addExcludedFolder(pathWithStatus.path);
    }

    this.requestUpdate();
  }

  #onDelete(idx: string): void {
    const index = Number.parseInt(idx, 10);
    if (index < 0 || index >= this.#excludedFolderPaths.length) {
      return;
    }

    this.#fileSystem?.removeExcludedFolder(this.#excludedFolderPaths[index].path);
    this.#excludedFolderPaths.splice(index, 1);

    this.requestUpdate();
  }

  #validateFolder(rawInput: string): PathWithStatus {
    const path = EditFileSystemView.#normalizePrefix(rawInput.trim()) as Platform.DevToolsPath.EncodedPathString;
    if (!path) {
      return {path, status: ExcludedFolderStatus.ERROR_NOT_A_PATH};
    }

    if (this.#excludedFolderPaths.findIndex(({path: p}) => p === path) !== -1) {
      return {path, status: ExcludedFolderStatus.ERROR_NOT_UNIQUE};
    }

    return {path, status: ExcludedFolderStatus.VALID};
  }

  static #normalizePrefix(prefix: string): string {
    if (!prefix) {
      return '';
    }
    return prefix + (prefix[prefix.length - 1] === '/' ? '' : '/');
  }
}
