// 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 * as Common from '../../core/common/common.js';
import * as Host from '../../core/host/host.js';
import * as Platform from '../../core/platform/platform.js';
import * as TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';

import type {IsolatedFileSystem} from './IsolatedFileSystem.js';
import {Events, type IsolatedFileSystemManager} from './IsolatedFileSystemManager.js';
import type {PlatformFileSystem, PlatformFileSystemType} from './PlatformFileSystem.js';

export class FileSystemWorkspaceBinding {
  readonly isolatedFileSystemManager: IsolatedFileSystemManager;
  readonly #workspace: Workspace.Workspace.WorkspaceImpl;
  readonly #eventListeners: Common.EventTarget.EventDescriptor[];
  readonly #boundFileSystems = new Map<string, FileSystem>();
  constructor(isolatedFileSystemManager: IsolatedFileSystemManager, workspace: Workspace.Workspace.WorkspaceImpl) {
    this.isolatedFileSystemManager = isolatedFileSystemManager;
    this.#workspace = workspace;
    this.#eventListeners = [
      this.isolatedFileSystemManager.addEventListener(Events.FileSystemAdded, this.onFileSystemAdded, this),
      this.isolatedFileSystemManager.addEventListener(Events.FileSystemRemoved, this.onFileSystemRemoved, this),
      this.isolatedFileSystemManager.addEventListener(Events.FileSystemFilesChanged, this.fileSystemFilesChanged, this),
    ];
    void this.isolatedFileSystemManager.waitForFileSystems().then(this.onFileSystemsLoaded.bind(this));
  }

  static projectId(fileSystemPath: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString {
    return fileSystemPath;
  }

  static relativePath(uiSourceCode: Workspace.UISourceCode.UISourceCode): Platform.DevToolsPath.EncodedPathString[] {
    const baseURL = (uiSourceCode.project() as FileSystem).fileSystemBaseURL;
    return Common.ParsedURL.ParsedURL.split(
        Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(uiSourceCode.url(), baseURL.length), '/');
  }

  static tooltipForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
    const fileSystem = (uiSourceCode.project() as FileSystem).fileSystem();
    return fileSystem.tooltipForURL(uiSourceCode.url());
  }

  static fileSystemType(project: Workspace.Workspace.Project): PlatformFileSystemType {
    if (project instanceof FileSystem) {
      return project.fileSystem().type();
    }
    throw new TypeError('project is not a FileSystem');
  }

  static fileSystemSupportsAutomapping(project: Workspace.Workspace.Project): boolean {
    const fileSystem = (project as FileSystem).fileSystem();
    return fileSystem.supportsAutomapping();
  }

  static completeURL(project: Workspace.Workspace.Project, relativePath: string): Platform.DevToolsPath.UrlString {
    const fsProject = project as FileSystem;
    return Common.ParsedURL.ParsedURL.concatenate(fsProject.fileSystemBaseURL, relativePath);
  }

  static fileSystemPath(projectId: Platform.DevToolsPath.UrlString): Platform.DevToolsPath.UrlString {
    return projectId;
  }

  private onFileSystemsLoaded(fileSystems: IsolatedFileSystem[]): void {
    for (const fileSystem of fileSystems) {
      this.addFileSystem(fileSystem);
    }
  }

  private onFileSystemAdded(event: Common.EventTarget.EventTargetEvent<PlatformFileSystem>): void {
    const fileSystem = event.data;
    this.addFileSystem(fileSystem);
  }

  private addFileSystem(fileSystem: PlatformFileSystem): void {
    const boundFileSystem = new FileSystem(this, fileSystem, this.#workspace);
    this.#boundFileSystems.set(fileSystem.path(), boundFileSystem);
  }

  private onFileSystemRemoved(event: Common.EventTarget.EventTargetEvent<PlatformFileSystem>): void {
    const fileSystem = event.data;
    const boundFileSystem = this.#boundFileSystems.get(fileSystem.path());
    if (boundFileSystem) {
      boundFileSystem.dispose();
    }
    this.#boundFileSystems.delete(fileSystem.path());
  }

  private fileSystemFilesChanged(event: Common.EventTarget.EventTargetEvent<FilesChangedData>): void {
    const paths = event.data;
    for (const fileSystemPath of paths.changed.keysArray()) {
      const fileSystem = this.#boundFileSystems.get(fileSystemPath);
      if (!fileSystem) {
        continue;
      }
      paths.changed.get(fileSystemPath).forEach(path => fileSystem.fileChanged(path));
    }

    for (const fileSystemPath of paths.added.keysArray()) {
      const fileSystem = this.#boundFileSystems.get(fileSystemPath);
      if (!fileSystem) {
        continue;
      }
      paths.added.get(fileSystemPath).forEach(path => fileSystem.fileChanged(path));
    }

    for (const fileSystemPath of paths.removed.keysArray()) {
      const fileSystem = this.#boundFileSystems.get(fileSystemPath);
      if (!fileSystem) {
        continue;
      }
      paths.removed.get(fileSystemPath).forEach(path => fileSystem.removeUISourceCode(path));
    }
  }

  dispose(): void {
    Common.EventTarget.removeEventListeners(this.#eventListeners);
    for (const fileSystem of this.#boundFileSystems.values()) {
      fileSystem.dispose();
      this.#boundFileSystems.delete(fileSystem.fileSystem().path());
    }
  }
}

export class FileSystem extends Workspace.Workspace.ProjectStore {
  #fileSystem: PlatformFileSystem;
  readonly fileSystemBaseURL: Platform.DevToolsPath.UrlString;
  readonly #fileSystemParentURL: Platform.DevToolsPath.UrlString;
  readonly #fileSystemWorkspaceBinding: FileSystemWorkspaceBinding;
  readonly #fileSystemPath: Platform.DevToolsPath.UrlString;
  readonly #creatingFilesGuard = new Set<string>();

  constructor(
      fileSystemWorkspaceBinding: FileSystemWorkspaceBinding, isolatedFileSystem: PlatformFileSystem,
      workspace: Workspace.Workspace.WorkspaceImpl) {
    const fileSystemPath = isolatedFileSystem.path();
    const id = FileSystemWorkspaceBinding.projectId(fileSystemPath);
    console.assert(!workspace.project(id));
    const displayName = fileSystemPath.substr(fileSystemPath.lastIndexOf('/') + 1);

    super(workspace, id, Workspace.Workspace.projectTypes.FileSystem, displayName);

    this.#fileSystem = isolatedFileSystem;
    this.fileSystemBaseURL = Common.ParsedURL.ParsedURL.concatenate(this.#fileSystem.path(), '/');
    this.#fileSystemParentURL =
        Common.ParsedURL.ParsedURL.substr(this.fileSystemBaseURL, 0, fileSystemPath.lastIndexOf('/') + 1);
    this.#fileSystemWorkspaceBinding = fileSystemWorkspaceBinding;
    this.#fileSystemPath = fileSystemPath;

    workspace.addProject(this);
    this.populate();
  }

  fileSystemPath(): Platform.DevToolsPath.UrlString {
    return this.#fileSystemPath;
  }

  fileSystem(): PlatformFileSystem {
    return this.#fileSystem;
  }

  mimeType(uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
    return this.#fileSystem.mimeFromPath(uiSourceCode.url());
  }

  initialGitFolders(): Platform.DevToolsPath.UrlString[] {
    return this.#fileSystem.initialGitFolders().map(
        folder => Common.ParsedURL.ParsedURL.concatenate(this.#fileSystemPath, '/', folder));
  }

  private filePathForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode):
      Platform.DevToolsPath.EncodedPathString {
    return Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(uiSourceCode.url(), this.#fileSystemPath.length);
  }

  isServiceProject(): boolean {
    return false;
  }

  requestMetadata(uiSourceCode: Workspace.UISourceCode.UISourceCode):
      Promise<Workspace.UISourceCode.UISourceCodeMetadata|null> {
    const metadata = sourceCodeToMetadataMap.get(uiSourceCode);
    if (metadata) {
      return metadata;
    }
    const relativePath = this.filePathForUISourceCode(uiSourceCode);
    const promise = this.#fileSystem.getMetadata(relativePath).then(onMetadata);
    sourceCodeToMetadataMap.set(uiSourceCode, promise);
    return promise;

    function onMetadata(metadata: {modificationTime: Date, size: number}|null):
        Workspace.UISourceCode.UISourceCodeMetadata|null {
      if (!metadata) {
        return null;
      }
      return new Workspace.UISourceCode.UISourceCodeMetadata(metadata.modificationTime, metadata.size);
    }
  }

  requestFileBlob(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<Blob|null> {
    return this.#fileSystem.requestFileBlob(this.filePathForUISourceCode(uiSourceCode));
  }

  requestFileContent(uiSourceCode: Workspace.UISourceCode.UISourceCode):
      Promise<TextUtils.ContentData.ContentDataOrError> {
    const filePath = this.filePathForUISourceCode(uiSourceCode);
    return this.#fileSystem.requestFileContent(filePath);
  }

  canSetFileContent(): boolean {
    return true;
  }

  async setFileContent(uiSourceCode: Workspace.UISourceCode.UISourceCode, newContent: string, isBase64: boolean):
      Promise<void> {
    const filePath = this.filePathForUISourceCode(uiSourceCode);
    this.#fileSystem.setFileContent(filePath, newContent, isBase64);
  }

  fullDisplayName(uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
    const baseURL = (uiSourceCode.project() as FileSystem).#fileSystemParentURL;
    return uiSourceCode.url().substring(baseURL.length);
  }

  canRename(): boolean {
    return true;
  }

  override rename(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, newName: Platform.DevToolsPath.RawPathString,
      callback:
          (arg0: boolean, arg1?: string|undefined, arg2?: Platform.DevToolsPath.UrlString|undefined,
           arg3?: Common.ResourceType.ResourceType|undefined) => void): void {
    if (newName === uiSourceCode.name()) {
      callback(true, uiSourceCode.name(), uiSourceCode.url(), uiSourceCode.contentType());
      return;
    }

    let filePath = this.filePathForUISourceCode(uiSourceCode);
    this.#fileSystem.renameFile(filePath, newName, innerCallback.bind(this));

    function innerCallback(this: FileSystem, success: boolean, newName?: string): void {
      if (!success || !newName) {
        callback(false, newName);
        return;
      }
      console.assert(Boolean(newName));
      const slash = filePath.lastIndexOf('/');
      const parentPath = Common.ParsedURL.ParsedURL.substr(filePath, 0, slash);
      filePath = Common.ParsedURL.ParsedURL.encodedFromParentPathAndName(parentPath, newName);
      filePath = Common.ParsedURL.ParsedURL.substr(filePath, 1);
      const newURL = Common.ParsedURL.ParsedURL.concatenate(this.fileSystemBaseURL, filePath);
      const newContentType = this.#fileSystem.contentType(newName);
      this.renameUISourceCode(uiSourceCode, newName);
      callback(true, newName, newURL, newContentType);
    }
  }

  async searchInFileContent(
      uiSourceCode: Workspace.UISourceCode.UISourceCode, query: string, caseSensitive: boolean,
      isRegex: boolean): Promise<TextUtils.ContentProvider.SearchMatch[]> {
    const filePath = this.filePathForUISourceCode(uiSourceCode);
    const content = await this.#fileSystem.requestFileContent(filePath);
    return TextUtils.TextUtils.performSearchInContentData(content, query, caseSensitive, isRegex);
  }

  async findFilesMatchingSearchRequest(
      searchConfig: Workspace.SearchConfig.SearchConfig, filesMatchingFileQuery: Workspace.UISourceCode.UISourceCode[],
      progress: Common.Progress.Progress):
      Promise<Map<Workspace.UISourceCode.UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>> {
    let workingFileSet: string[] = filesMatchingFileQuery.map(uiSoureCode => uiSoureCode.url());
    const queriesToRun = searchConfig.queries().slice();
    if (!queriesToRun.length) {
      queriesToRun.push('');
    }
    progress.totalWork = queriesToRun.length;

    for (const query of queriesToRun) {
      const files = await this.#fileSystem.searchInPath(searchConfig.isRegex() ? '' : query, progress);
      files.sort(Platform.StringUtilities.naturalOrderComparator);
      workingFileSet = Platform.ArrayUtilities.intersectOrdered(
          workingFileSet, files, Platform.StringUtilities.naturalOrderComparator);
      ++progress.worked;
    }

    const result = new Map();
    for (const file of workingFileSet) {
      const uiSourceCode = this.uiSourceCodeForURL(file as Platform.DevToolsPath.UrlString);
      if (uiSourceCode) {
        result.set(uiSourceCode, null);
      }
    }

    progress.done = true;
    return result;
  }

  override indexContent(progress: Common.Progress.Progress): void {
    this.#fileSystem.indexContent(progress);
  }

  populate(): void {
    const filePaths = this.#fileSystem.initialFilePaths();
    if (filePaths.length === 0) {
      return;
    }

    const chunkSize = 1000;
    const startTime = performance.now();
    reportFileChunk.call(this, 0);

    function reportFileChunk(this: FileSystem, from: number): void {
      const to = Math.min(from + chunkSize, filePaths.length);
      for (let i = from; i < to; ++i) {
        this.addFile(filePaths[i]);
      }
      if (to < filePaths.length) {
        window.setTimeout(reportFileChunk.bind(this, to), 100);
      } else if (this.type() === 'filesystem') {
        Host.userMetrics.workspacesPopulated(performance.now() - startTime);
      }
    }
  }

  override excludeFolder(url: Platform.DevToolsPath.UrlString): void {
    let relativeFolder = Common.ParsedURL.ParsedURL.sliceUrlToEncodedPathString(url, this.fileSystemBaseURL.length);
    if (!relativeFolder.startsWith('/')) {
      relativeFolder = Common.ParsedURL.ParsedURL.prepend('/', relativeFolder);
    }
    if (!relativeFolder.endsWith('/')) {
      relativeFolder = Common.ParsedURL.ParsedURL.concatenate(relativeFolder, '/');
    }
    this.#fileSystem.addExcludedFolder(relativeFolder);

    for (const uiSourceCode of this.uiSourceCodes()) {
      if (uiSourceCode.url().startsWith(url)) {
        this.removeUISourceCode(uiSourceCode.url());
      }
    }
  }

  canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean {
    return this.#fileSystem.canExcludeFolder(path);
  }

  canCreateFile(): boolean {
    return true;
  }

  async createFile(
      path: Platform.DevToolsPath.EncodedPathString, name: Platform.DevToolsPath.RawPathString|null, content: string,
      isBase64?: boolean): Promise<Workspace.UISourceCode.UISourceCode|null> {
    const guardFileName = this.#fileSystemPath + path + (!path.endsWith('/') ? '/' : '') + name;
    this.#creatingFilesGuard.add(guardFileName);
    const filePath = await this.#fileSystem.createFile(path, name);
    if (!filePath) {
      return null;
    }
    const uiSourceCode = this.addFile(filePath, content, isBase64);
    this.#creatingFilesGuard.delete(guardFileName);
    return uiSourceCode;
  }

  override deleteFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
    const relativePath = this.filePathForUISourceCode(uiSourceCode);
    void this.#fileSystem.deleteFile(relativePath).then(success => {
      if (success) {
        this.removeUISourceCode(uiSourceCode.url());
      }
    });
  }

  override deleteDirectoryRecursively(path: Platform.DevToolsPath.EncodedPathString): Promise<boolean> {
    return this.#fileSystem.deleteDirectoryRecursively(path);
  }

  override remove(): void {
    this.#fileSystemWorkspaceBinding.isolatedFileSystemManager.removeFileSystem(this.#fileSystem);
  }

  private addFile(filePath: Platform.DevToolsPath.EncodedPathString, content?: string, isBase64?: boolean):
      Workspace.UISourceCode.UISourceCode {
    const contentType = this.#fileSystem.contentType(filePath);
    const uiSourceCode =
        this.createUISourceCode(Common.ParsedURL.ParsedURL.concatenate(this.fileSystemBaseURL, filePath), contentType);
    if (content !== undefined) {
      uiSourceCode.setContent(content, Boolean(isBase64));
    }
    this.addUISourceCode(uiSourceCode);
    return uiSourceCode;
  }

  fileChanged(path: Platform.DevToolsPath.UrlString): void {
    // Ignore files that are being created but do not have content yet.
    if (this.#creatingFilesGuard.has(path)) {
      return;
    }
    const uiSourceCode = this.uiSourceCodeForURL(path);
    if (!uiSourceCode) {
      const contentType = this.#fileSystem.contentType(path);
      this.addUISourceCode(this.createUISourceCode(path, contentType));
      return;
    }
    sourceCodeToMetadataMap.delete(uiSourceCode);
    void uiSourceCode.checkContentUpdated();
  }

  tooltipForURL(url: Platform.DevToolsPath.UrlString): string {
    return this.#fileSystem.tooltipForURL(url);
  }

  dispose(): void {
    this.removeProject();
  }
}

const sourceCodeToMetadataMap =
    new WeakMap<Workspace.UISourceCode.UISourceCode, Promise<Workspace.UISourceCode.UISourceCodeMetadata|null>>();
export interface FilesChangedData {
  changed: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>;
  added: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>;
  removed: Platform.MapUtilities.Multimap<Platform.DevToolsPath.UrlString, Platform.DevToolsPath.UrlString>;
}
