import { URI as Uri } from 'vscode-uri';

import { IRelativePattern, match } from './glob';
import { Path } from './path';

export { URI as Uri, Utils as UriUtils } from 'vscode-uri';

// 3.0 的 vscode-uri 没有导出这个
export interface UriComponents {
  scheme: string;
  authority: string;
  path: string;
  query: string;
  fragment: string;
}

export class URI {
  static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }) {
    return new URI(Uri.from(components));
  }

  static file(path: string) {
    return new URI(Uri.file(path));
  }

  static parse(path: string) {
    return new URI(Uri.parse(path));
  }

  static isUri(thing: any): thing is URI {
    if (thing instanceof URI) {
      return true;
    }
    if (!thing) {
      return false;
    }
    return Uri.isUri(thing);
  }

  static isUriString(str: string): boolean {
    return !!str && /^[A-Za-z\-\_]+:\/\//.test(str);
  }

  static revive(data: any) {
    return Uri.revive(data);
  }

  public readonly codeUri: Uri;
  private _path: Path | undefined;

  constructor(uri: string | Uri = '') {
    if (Uri.isUri(uri)) {
      this.codeUri = uri;
    } else {
      this.codeUri = Uri.parse(uri);
    }
  }

  get displayName(): string {
    const base = this.path.base;
    if (base) {
      return base;
    }
    if (this.path.isRoot) {
      return this.path.toString();
    }
    return '';
  }

  /**
   * Return all uri from the current to the top most.
   */
  get allLocations(): URI[] {
    const locations: any[] = [];
    let location: URI = this;
    while (!location.path.isRoot) {
      locations.push(location);
      location = location.parent;
    }
    locations.push(location);
    return locations;
  }

  get parent(): URI {
    if (this.path.isRoot) {
      return this;
    }
    return this.withPath(this.path.dir);
  }

  relative(uri: URI): Path | undefined {
    if (this.authority !== uri.authority || this.scheme !== uri.scheme) {
      return undefined;
    }
    return this.path.relative(uri.path);
  }

  resolve(path: string | Path): URI {
    return this.withPath(this.path.join(path.toString()));
  }

  /**
   * return a new URI replacing the current with the given scheme
   */
  withScheme(scheme: string): URI {
    const newCodeUri = Uri.from({
      ...this.codeUri.toJSON(),
      scheme,
    });
    return new URI(newCodeUri);
  }

  /**
   * @deprecated
   * return this URI without a scheme
   */
  withoutScheme(): URI {
    /**
     * 本方法已废弃
     * 由于 vscode-uri 自 1.0.8 起，内部强制校验和修正 `scheme` 参数
     * 会将 scheme 为空时强制修正为 `file` (不可关闭)
     * 会在不传入 scheme 时报错 (可通过 setUriThrowOnMissingScheme 关闭)
     * 因此这里直接调用 `new Uri` 产生一个不带 scheme 的 Uri 实例
     * ---- !!!!!!! ATTENTION ----
     * 这样做的影响是 Uri 的实例 toJSON 时没有 $mid 参数，会在通信序列化时无法传输
     * ---- !!!!!!! ATTENTION ----
     * 但是 vscode-uri 对 constructor 属性加了 protected 保护，因此增加了 `ts-ignore`
     */
    if (process.env.NODE_ENV !== 'production' && console !== undefined) {
      // eslint-disable-next-line no-console
      console.warn(
        'Warning: `Uri.withoutScheme` is deprecated, ' +
          'If you want to get `fsPath` by `withoutScheme` method, ' +
          "it's recommended to use `uri.path.toString` directly",
      );
    }

    // @ts-ignore
    const newCodeUri = new Uri({
      ...this.codeUri.toJSON(),
      scheme: '',
    });

    return new URI(newCodeUri);
  }

  /**
   * return a new URI replacing the current with the given authority
   */
  withAuthority(authority: string): URI {
    const newCodeUri = Uri.from({
      ...this.codeUri.toJSON(),
      scheme: this.codeUri.scheme,
      authority,
    });
    return new URI(newCodeUri);
  }

  /**
   * return this URI without a authority
   */
  withoutAuthority(): URI {
    return this.withAuthority('');
  }

  /**
   * return a new URI replacing the current with the given path
   */
  withPath(path: string | Path): URI {
    const newCodeUri = Uri.from({
      ...this.codeUri.toJSON(),
      scheme: this.codeUri.scheme,
      path: path.toString(),
    });
    return new URI(newCodeUri);
  }

  /**
   * return this URI without a path
   */
  withoutPath(): URI {
    return this.withPath('');
  }

  /**
   * return a new URI replacing the current with the given query
   */
  withQuery(query: string): URI {
    const newCodeUri = Uri.from({
      ...this.codeUri.toJSON(),
      scheme: this.codeUri.scheme,
      query,
    });
    return new URI(newCodeUri);
  }

  /**
   * return this URI without a query
   */
  withoutQuery(): URI {
    return this.withQuery('');
  }

  /**
   * return a new URI replacing the current with the given fragment
   */
  withFragment(fragment: string): URI {
    const newCodeUri = Uri.from({
      ...this.codeUri.toJSON(),
      scheme: this.codeUri.scheme,
      fragment,
    });
    return new URI(newCodeUri);
  }

  /**
   * return this URI without a fragment
   */
  withoutFragment(): URI {
    return this.withFragment('');
  }

  get scheme(): string {
    return this.codeUri.scheme;
  }

  get authority(): string {
    return this.codeUri.authority;
  }

  get path(): Path {
    if (this._path === undefined) {
      this._path = new Path(this.codeUri.path);
    }
    return this._path;
  }

  get query(): string {
    return this.codeUri.query;
  }

  get fragment(): string {
    return this.codeUri.fragment;
  }

  toString(skipEncoding?: boolean) {
    return this.codeUri.toString(skipEncoding);
  }

  toJSON() {
    return {
      ...this.codeUri.toJSON(),
      // `$mid === 1` will cause URI to be deserialized as a Uri
      $mid: undefined,
    };
  }

  isEqualOrParent(uri: URI): boolean {
    return this.authority === uri.authority && this.scheme === uri.scheme && this.path.isEqualOrParent(uri.path);
  }

  isEqual(uri: URI): boolean {
    return (
      this.authority === uri.authority &&
      this.scheme === uri.scheme &&
      this.path.isEqual(uri.path) &&
      this.query === uri.query
    );
  }

  matchGlobPattern(pattern: string | IRelativePattern): boolean {
    return match(pattern, this.path.toString());
  }

  static getDistinctParents(uris: URI[]): URI[] {
    const result: URI[] = [];
    uris.forEach((uri, i) => {
      if (!uris.some((otherUri, index) => index !== i && otherUri.isEqualOrParent(uri))) {
        result.push(uri);
      }
    });
    return result;
  }

  getParsedQuery(): { [key: string]: string } {
    const queryString = this.query;
    const query = {};
    const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
    // eslint-disable-next-line @typescript-eslint/prefer-for-of
    for (let i = 0; i < pairs.length; i++) {
      const pair = pairs[i].split('=');
      query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
    }
    return query;
  }

  static stringifyQuery(query: { [key: string]: any }): string {
    const values: string[] = [];
    Object.keys(query).forEach((key) => {
      const value = encodeURIComponent(query[key]);
      if (value !== undefined) {
        values.push(encodeURIComponent(key) + '=' + value);
      }
    });
    return values.join('&');
  }

  with(parsed: Partial<UriComponents>): URI {
    return new URI(Uri.from({ ...this.codeUri.toJSON(), ...parsed }));
  }
}
