// Copyright 2012 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 type * as Platform from '../../core/platform/platform.js';
import * as Root from '../../core/root/root.js';
import type * as TextUtils from '../text_utils/text_utils.js';

import type {SearchConfig} from './SearchConfig.js';
import {UISourceCode, type UISourceCodeMetadata} from './UISourceCode.js';

export interface Project {
  workspace(): WorkspaceImpl;
  id(): string;
  type(): projectTypes;
  isServiceProject(): boolean;
  displayName(): string;
  requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
  requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentData.ContentDataOrError>;
  canSetFileContent(): boolean;
  setFileContent(uiSourceCode: UISourceCode, newContent: string, isBase64: boolean): Promise<void>;
  fullDisplayName(uiSourceCode: UISourceCode): string;
  mimeType(uiSourceCode: UISourceCode): string;
  canRename(): boolean;
  rename(
      uiSourceCode: UISourceCode, newName: Platform.DevToolsPath.RawPathString,
      callback:
          (arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString,
           arg3?: Common.ResourceType.ResourceType) => void): void;
  excludeFolder(path: Platform.DevToolsPath.UrlString): void;
  canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean;
  createFile(path: Platform.DevToolsPath.EncodedPathString, name: string|null, content: string, isBase64?: boolean):
      Promise<UISourceCode|null>;
  canCreateFile(): boolean;
  deleteFile(uiSourceCode: UISourceCode): void;
  deleteDirectoryRecursively(path: Platform.DevToolsPath.EncodedPathString): Promise<boolean>;
  remove(): void;
  removeUISourceCode(url: Platform.DevToolsPath.UrlString): void;
  searchInFileContent(uiSourceCode: UISourceCode, query: string, caseSensitive: boolean, isRegex: boolean):
      Promise<TextUtils.ContentProvider.SearchMatch[]>;
  findFilesMatchingSearchRequest(
      searchConfig: SearchConfig, filesMatchingFileQuery: UISourceCode[],
      progress: Common.Progress.Progress): Promise<Map<UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>>;
  indexContent(progress: Common.Progress.Progress): void;
  uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null;

  /**
   * Returns an iterator for the currently registered {@link UISourceCode}s for this project. When
   * new {@link UISourceCode}s are added while iterating, they might show up already. When removing
   * {@link UISourceCode}s while iterating, these will no longer show up, and will have no effect
   * on the other entries.
   *
   * @returns an iterator for the sources provided by this project.
   */
  uiSourceCodes(): Iterable<UISourceCode>;
}

/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
export enum projectTypes {
  Debugger = 'debugger',
  Formatter = 'formatter',
  Network = 'network',
  FileSystem = 'filesystem',
  ConnectableFileSystem = 'connectablefilesystem',
  ContentScripts = 'contentscripts',
  Service = 'service',
}
/* eslint-enable @typescript-eslint/naming-convention */

export abstract class ProjectStore implements Project {
  readonly #workspace: WorkspaceImpl;
  readonly #id: string;
  readonly #type: projectTypes;
  readonly #displayName: string;
  readonly #uiSourceCodes = new Map<Platform.DevToolsPath.UrlString, UISourceCode>();

  constructor(workspace: WorkspaceImpl, id: string, type: projectTypes, displayName: string) {
    this.#workspace = workspace;
    this.#id = id;
    this.#type = type;
    this.#displayName = displayName;
  }

  id(): string {
    return this.#id;
  }

  type(): projectTypes {
    return this.#type;
  }

  displayName(): string {
    return this.#displayName;
  }

  workspace(): WorkspaceImpl {
    return this.#workspace;
  }

  createUISourceCode(url: Platform.DevToolsPath.UrlString, contentType: Common.ResourceType.ResourceType):
      UISourceCode {
    return new UISourceCode(this, url, contentType);
  }

  addUISourceCode(uiSourceCode: UISourceCode): boolean {
    const url = uiSourceCode.url();
    if (this.uiSourceCodeForURL(url)) {
      return false;
    }
    this.#uiSourceCodes.set(url, uiSourceCode);
    this.#workspace.dispatchEventToListeners(Events.UISourceCodeAdded, uiSourceCode);
    return true;
  }

  removeUISourceCode(url: Platform.DevToolsPath.UrlString): void {
    const uiSourceCode = this.#uiSourceCodes.get(url);
    if (uiSourceCode === undefined) {
      return;
    }
    this.#uiSourceCodes.delete(url);
    this.#workspace.dispatchEventToListeners(Events.UISourceCodeRemoved, uiSourceCode);
  }

  removeProject(): void {
    this.#workspace.removeProject(this);
    this.#uiSourceCodes.clear();
  }

  uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
    return this.#uiSourceCodes.get(url) ?? null;
  }

  uiSourceCodes(): Iterable<UISourceCode> {
    return this.#uiSourceCodes.values();
  }

  renameUISourceCode(uiSourceCode: UISourceCode, newName: string): void {
    const oldPath = uiSourceCode.url();
    const newPath = uiSourceCode.parentURL() ?
        Common.ParsedURL.ParsedURL.urlFromParentUrlAndName(uiSourceCode.parentURL(), newName) :
        Common.ParsedURL.ParsedURL.preEncodeSpecialCharactersInPath(newName) as Platform.DevToolsPath.UrlString;
    this.#uiSourceCodes.set(newPath, uiSourceCode);
    this.#uiSourceCodes.delete(oldPath);
  }

  // No-op implementation for a handful of interface methods.

  rename(
      _uiSourceCode: UISourceCode, _newName: string,
      _callback:
          (arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString,
           arg3?: Common.ResourceType.ResourceType) => void): void {
  }
  excludeFolder(_path: Platform.DevToolsPath.UrlString): void {
  }
  deleteFile(_uiSourceCode: UISourceCode): void {
  }
  deleteDirectoryRecursively(_path: Platform.DevToolsPath.EncodedPathString): Promise<boolean> {
    return Promise.resolve(false);
  }
  remove(): void {
  }
  indexContent(_progress: Common.Progress.Progress): void {
  }

  abstract isServiceProject(): boolean;
  abstract requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
  abstract requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentData.ContentDataOrError>;
  abstract canSetFileContent(): boolean;
  abstract setFileContent(uiSourceCode: UISourceCode, newContent: string, isBase64: boolean): Promise<void>;
  abstract fullDisplayName(uiSourceCode: UISourceCode): string;
  abstract mimeType(uiSourceCode: UISourceCode): string;
  abstract canRename(): boolean;
  abstract canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean;
  abstract createFile(
      path: Platform.DevToolsPath.EncodedPathString, name: string|null, content: string,
      isBase64?: boolean): Promise<UISourceCode|null>;
  abstract canCreateFile(): boolean;
  abstract searchInFileContent(uiSourceCode: UISourceCode, query: string, caseSensitive: boolean, isRegex: boolean):
      Promise<TextUtils.ContentProvider.SearchMatch[]>;
  abstract findFilesMatchingSearchRequest(
      searchConfig: SearchConfig, filesMatchingFileQuery: UISourceCode[],
      progress: Common.Progress.Progress): Promise<Map<UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>>;
}

export class WorkspaceImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
  #projects = new Map<string, Project>();
  #hasResourceContentTrackingExtensions = false;

  static instance(opts: {forceNew: boolean|null} = {forceNew: null}): WorkspaceImpl {
    const {forceNew} = opts;
    if (!Root.DevToolsContext.globalInstance().has(WorkspaceImpl) || forceNew) {
      Root.DevToolsContext.globalInstance().set(WorkspaceImpl, new WorkspaceImpl());
    }

    return Root.DevToolsContext.globalInstance().get(WorkspaceImpl);
  }

  static removeInstance(): void {
    Root.DevToolsContext.globalInstance().delete(WorkspaceImpl);
  }

  uiSourceCode(projectId: string, url: Platform.DevToolsPath.UrlString): UISourceCode|null {
    const project = this.#projects.get(projectId);
    return project ? project.uiSourceCodeForURL(url) : null;
  }

  uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
    for (const project of this.#projects.values()) {
      const uiSourceCode = project.uiSourceCodeForURL(url);
      if (uiSourceCode) {
        return uiSourceCode;
      }
    }
    return null;
  }

  findCompatibleUISourceCodes(uiSourceCode: UISourceCode): UISourceCode[] {
    const url = uiSourceCode.url();
    const contentType = uiSourceCode.contentType();
    const result: UISourceCode[] = [];
    for (const project of this.#projects.values()) {
      if (uiSourceCode.project().type() !== project.type()) {
        continue;
      }
      const candidate = project.uiSourceCodeForURL(url);
      if (candidate && candidate.url() === url && candidate.contentType() === contentType) {
        result.push(candidate);
      }
    }
    return result;
  }

  uiSourceCodesForProjectType(type: projectTypes): UISourceCode[] {
    const result: UISourceCode[] = [];
    for (const project of this.#projects.values()) {
      if (project.type() === type) {
        for (const uiSourceCode of project.uiSourceCodes()) {
          result.push(uiSourceCode);
        }
      }
    }
    return result;
  }

  addProject(project: Project): void {
    console.assert(!this.#projects.has(project.id()), `A project with id ${project.id()} already exists!`);
    this.#projects.set(project.id(), project);
    this.dispatchEventToListeners(Events.ProjectAdded, project);
  }

  removeProject(project: Project): void {
    this.#projects.delete(project.id());
    this.dispatchEventToListeners(Events.ProjectRemoved, project);
  }

  project(projectId: string): Project|null {
    return this.#projects.get(projectId) || null;
  }

  projectForFileSystemRoot(root: Platform.DevToolsPath.RawPathString): Project|null {
    const projectId = Common.ParsedURL.ParsedURL.rawPathToUrlString(root);
    return this.project(projectId);
  }

  projects(): Project[] {
    return [...this.#projects.values()];
  }

  projectsForType(type: projectTypes): Project[] {
    function filterByType(project: Project): boolean {
      return project.type() === type;
    }
    return this.projects().filter(filterByType);
  }

  uiSourceCodes(): UISourceCode[] {
    const result: UISourceCode[] = [];
    for (const project of this.#projects.values()) {
      for (const uiSourceCode of project.uiSourceCodes()) {
        result.push(uiSourceCode);
      }
    }
    return result;
  }

  setHasResourceContentTrackingExtensions(hasExtensions: boolean): void {
    this.#hasResourceContentTrackingExtensions = hasExtensions;
  }

  hasResourceContentTrackingExtensions(): boolean {
    return this.#hasResourceContentTrackingExtensions;
  }
}

export enum Events {
  /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
  UISourceCodeAdded = 'UISourceCodeAdded',
  UISourceCodeRemoved = 'UISourceCodeRemoved',
  UISourceCodeRenamed = 'UISourceCodeRenamed',
  WorkingCopyChanged = 'WorkingCopyChanged',
  WorkingCopyCommitted = 'WorkingCopyCommitted',
  WorkingCopyCommittedByUser = 'WorkingCopyCommittedByUser',
  ProjectAdded = 'ProjectAdded',
  ProjectRemoved = 'ProjectRemoved',
  /* eslint-enable @typescript-eslint/naming-convention */
}

export interface UISourceCodeRenamedEvent {
  oldURL: Platform.DevToolsPath.UrlString;
  uiSourceCode: UISourceCode;
}

export interface WorkingCopyChangedEvent {
  uiSourceCode: UISourceCode;
}

export interface WorkingCopyCommittedEvent {
  uiSourceCode: UISourceCode;
  content: string;
  encoded?: boolean;
}

export interface EventTypes {
  [Events.UISourceCodeAdded]: UISourceCode;
  [Events.UISourceCodeRemoved]: UISourceCode;
  [Events.UISourceCodeRenamed]: UISourceCodeRenamedEvent;
  [Events.WorkingCopyChanged]: WorkingCopyChangedEvent;
  [Events.WorkingCopyCommitted]: WorkingCopyCommittedEvent;
  [Events.WorkingCopyCommittedByUser]: WorkingCopyCommittedEvent;
  [Events.ProjectAdded]: Project;
  [Events.ProjectRemoved]: Project;
}
