// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/source/file_source.dart';
import 'package:analyzer/source/source.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';

PSEntry? _findEntry(
    YamlMap map, String key, ResourceProvider? resourceProvider) {
  PSEntry? entry;
  map.nodes.forEach((k, v) {
    if (k is YamlScalar && key == k.toString()) {
      entry = _processScalar(k, v, resourceProvider);
    }
  });
  return entry;
}

PSDependencyList? _processDependencies(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is! YamlMap) {
    return null;
  }

  _PSDependencyList deps = _PSDependencyList(_PSNode(key, resourceProvider));
  value.nodes.forEach((k, v) {
    if (k is YamlScalar) deps.add(_PSDependency(k, v, resourceProvider));
  });
  return deps;
}

PSEnvironment? _processEnvironment(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is! YamlMap) {
    return null;
  }

  _PSEnvironment env = _PSEnvironment(_PSNode(key, resourceProvider));
  env.flutter = _findEntry(value, 'flutter', resourceProvider);
  env.sdk = _findEntry(value, 'sdk', resourceProvider);
  return env;
}

PSGitRepo? _processGitRepo(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is YamlScalar) {
    _PSGitRepo repo = _PSGitRepo(_PSNode(key, resourceProvider));
    repo.url = PSEntry(repo.token, _PSNode(value, resourceProvider));
    return repo;
  }
  if (value is! YamlMap) {
    return null;
  }

  // url: git://github.com/munificent/kittens.git
  // ref: some-branch
  _PSGitRepo repo = _PSGitRepo(_PSNode(key, resourceProvider));
  repo.ref = _findEntry(value, 'ref', resourceProvider);
  repo.url = _findEntry(value, 'url', resourceProvider);
  return repo;
}

PSHost? _processHost(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is YamlScalar) {
    // dependencies:
    //   mypkg:
    //     hosted:  https://some-pub-server.com
    //     version: ^1.2.3
    _PSHost host = _PSHost(_PSNode(key, resourceProvider), isShortForm: true);
    host.url = _processScalar(key, value, resourceProvider);
    return host;
  }
  if (value is YamlMap) {
    // name: transmogrify
    // url: http://your-package-server.com
    _PSHost host = _PSHost(_PSNode(key, resourceProvider), isShortForm: false);
    host.name = _findEntry(value, 'name', resourceProvider);
    host.url = _findEntry(value, 'url', resourceProvider);
    return host;
  }
  return null;
}

PSEntry? _processScalar(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is! YamlScalar) {
    return null;
    //WARN?
  }
  return PSEntry(
      _PSNode(key, resourceProvider), _PSNode(value, resourceProvider));
}

PSNodeList? _processScalarList(
    YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
  if (value is! YamlList) {
    return null;
  }
  return _PSNodeList(
      _PSNode(key, resourceProvider),
      value.nodes
          .whereType<YamlScalar>()
          .map((n) => _PSNode(n, resourceProvider)));
}

/// Representation of a key/value pair a map from package name to
/// _package description_.
///
/// **Example** of a path-dependency:
/// ```yaml
/// dependencies:
///   <name>:
///     version: <version>
///     path: <path>
/// ```
abstract class PSDependency {
  PSGitRepo? get git;

  PSHost? get host;

  PSNode? get name;

  PSEntry? get path;

  PSEntry? get version;
}

/// Representation of the map from package name to _package description_ used
/// under `dependencies`, `dev_dependencies` and `dependency_overrides`.
abstract class PSDependencyList with IterableMixin<PSDependency> {}

class PSEntry {
  final PSNode? key;
  final PSNode value;

  PSEntry(this.key, this.value);

  @override
  String toString() => '${key != null ? '$key: ' : ''}$value';
}

/// Representation of environment in `pubspec.yaml`.
///
/// **Example** of an environment:
/// ```yaml
/// environment:
///   sdk: '>=2.12.0 <4.0.0'
///   flutter: ^3.3.10
/// ```
abstract class PSEnvironment {
  PSEntry? get flutter;

  PSEntry? get sdk;

  PSNode get token;
}

/// Representation of git-dependency in `pubspec.yaml`.
///
/// **Example** of a git-dependency:
/// ```yaml
/// dependencies:
///   foo:
///     git: # <-- this is the [token] property
///       url: https://github.com/example/example
///       ref: main # ref is optional
/// ```
///
/// This may also be written in the form:
/// ```yaml
/// dependencies:
///   foo:
///     git:       https://github.com/example/example
///     # ^-token  ^--url
///     # In this case [ref] is `null`.
/// ```
abstract class PSGitRepo {
  /// [PSEntry] for `ref: main` where [PSEntry.key] is `ref` and [PSEntry.value]
  /// is `main`.
  PSEntry? get ref;

  /// The `'git'` from the `pubspec.yaml`, this is the key that indicates this
  /// is a git-dependency.
  PSNode get token;

  /// [PSEntry] for `url: https://...` or `git: https://`, where [PSEntry.key]
  /// is either `url` or `git`, and [PSEntry.key] is the URL.
  ///
  /// If the git-dependency is given in the form:
  /// ```yaml
  /// dependencies:
  ///   foo:
  ///     git:       https://github.com/example/example
  /// ```
  /// Then [token] and [url.key] will be the same object.
  PSEntry? get url;
}

abstract class PSHost {
  /// True, if _short-form_ for writing hosted-dependencies was used.
  ///
  /// **Example** of a hosted-dependency written in short-form:
  /// ```yaml
  /// dependencies:
  ///   foo:
  ///     hosted: https://some-pub-server.com
  ///     version: ^1.2.3
  /// ```
  ///
  /// The _long-form_ for writing the dependency given above is:
  /// ```yaml
  /// dependencies:
  ///   foo:
  ///     hosted:
  ///       url: https://some-pub-server.com
  ///       name: foo
  ///     version: ^1.2.3
  /// ```
  ///
  /// The short-form was added in Dart 2.15.0 because:
  ///  * The `name` property just specifies the package name, which can be
  ///    inferred from the context. So it is unnecessary to write it.
  ///  * The nested object and `url` key becomes unnecessary when the `name`
  ///    property is removed.
  bool get isShortForm;

  PSEntry? get name;

  PSNode get token;

  PSEntry? get url;
}

/// Representation of a leaf-node from `pubspec.yaml`.
abstract class PSNode {
  Source get source;

  SourceSpan get span;

  /// String value of the node, or `null` if value in pubspec.yaml is `null` or
  /// omitted.
  ///
  /// **Example**
  /// ```
  /// name: foo
  /// version:
  /// ```
  /// In the example above the [PSNode] for `foo` will have [text] "foo", and
  /// the [PSNode] for `version` will have not have [text] as `null`, as empty
  /// value or `"null"` is the same in YAML.
  String? get text;
}

abstract class PSNodeList with IterableMixin<PSNode> {
  @override
  Iterator<PSNode> get iterator;

  PSNode get token;
}

abstract class Pubspec {
  factory Pubspec.parse(String pubspec,
      {Uri? sourceUrl, ResourceProvider? resourceProvider}) {
    try {
      var yaml = loadYamlNode(pubspec, sourceUrl: sourceUrl);
      return Pubspec.parseYaml(yaml, resourceProvider: resourceProvider);
    } on Exception {
      return _Pubspec(YamlMap(), resourceProvider: resourceProvider);
    }
  }

  factory Pubspec.parseYaml(YamlNode yaml,
      {ResourceProvider? resourceProvider}) {
    return _Pubspec(yaml, resourceProvider: resourceProvider);
  }

  PSEntry? get author;

  PSNodeList? get authors;

  PSDependencyList? get dependencies;

  PSDependencyList? get dependencyOverrides;

  PSEntry? get description;

  PSDependencyList? get devDependencies;

  PSEntry? get documentation;

  PSEnvironment? get environment;

  PSEntry? get homepage;

  PSEntry? get issueTracker;

  PSEntry? get name;

  PSEntry? get repository;

  PSEntry? get version;

  void accept(PubspecVisitor visitor);
}

abstract class PubspecVisitor<T> {
  T? visitPackageAuthor(PSEntry author) => null;

  T? visitPackageAuthors(PSNodeList authors) => null;

  T? visitPackageDependencies(PSDependencyList dependencies) => null;

  T? visitPackageDependency(PSDependency dependency) => null;

  T? visitPackageDependencyOverride(PSDependency dependency) => null;

  T? visitPackageDependencyOverrides(PSDependencyList dependencies) => null;

  T? visitPackageDescription(PSEntry description) => null;

  T? visitPackageDevDependencies(PSDependencyList dependencies) => null;

  T? visitPackageDevDependency(PSDependency dependency) => null;

  T? visitPackageDocumentation(PSEntry documentation) => null;

  T? visitPackageEnvironment(PSEnvironment environment) => null;

  T? visitPackageHomepage(PSEntry homepage) => null;

  T? visitPackageIssueTracker(PSEntry issueTracker) => null;

  T? visitPackageName(PSEntry name) => null;

  T? visitPackageRepository(PSEntry repository) => null;

  T? visitPackageVersion(PSEntry version) => null;
}

class _PSDependency extends PSDependency {
  @override
  PSNode? name;
  @override
  PSEntry? path;
  @override
  PSEntry? version;
  @override
  PSHost? host;
  @override
  PSGitRepo? git;

  factory _PSDependency(
      YamlScalar key, YamlNode value, ResourceProvider? resourceProvider) {
    _PSDependency dep = _PSDependency._();

    dep.name = _PSNode(key, resourceProvider);

    if (value is YamlScalar) {
      // Simple version
      dep.version = PSEntry(null, _PSNode(value, resourceProvider));
    } else if (value is YamlMap) {
      // hosted:
      //   name: transmogrify
      //   url: http://your-package-server.com
      //   version: '>=0.4.0 <1.0.0'
      YamlMap details = value;
      details.nodes.forEach((k, v) {
        if (k is! YamlScalar) {
          return;
        }
        YamlScalar key = k;
        switch (key.toString()) {
          case 'path':
            dep.path = _processScalar(key, v, resourceProvider);
          case 'version':
            dep.version = _processScalar(key, v, resourceProvider);
          case 'hosted':
            dep.host = _processHost(key, v, resourceProvider);
          case 'git':
            dep.git = _processGitRepo(key, v, resourceProvider);
        }
      });
    }
    return dep;
  }

  _PSDependency._();

  @override
  String toString() {
    var sb = StringBuffer();
    if (name != null) {
      sb.write('$name:');
    }
    var versionInfo = '';
    if (version != null) {
      if (version!.key == null) {
        versionInfo = ' $version';
      } else {
        versionInfo = '\n    $version';
      }
    }
    sb.writeln(versionInfo);
    if (host != null) {
      sb.writeln(host);
    }
    if (git != null) {
      sb.writeln(git);
    }
    return sb.toString();
  }
}

class _PSDependencyList extends PSDependencyList {
  final dependencies = <PSDependency>[];
  final PSNode token;

  _PSDependencyList(this.token);

  @override
  Iterator<PSDependency> get iterator => dependencies.iterator;

  void add(PSDependency? dependency) {
    if (dependency != null) {
      dependencies.add(dependency);
    }
  }

  @override
  String toString() => '$token\n${dependencies.join('  ')}';
}

class _PSEnvironment implements PSEnvironment {
  @override
  PSNode token;
  @override
  PSEntry? flutter;
  @override
  PSEntry? sdk;

  _PSEnvironment(this.token);

  @override
  String toString() => '''
    $token:
      $sdk
      $flutter''';
}

class _PSGitRepo implements PSGitRepo {
  @override
  PSNode token;
  @override
  PSEntry? ref;
  @override
  PSEntry? url;

  _PSGitRepo(this.token);

  @override
  String toString() => '''
    $token:
      $url
      $ref''';
}

class _PSHost implements PSHost {
  @override
  bool isShortForm;

  @override
  PSEntry? name;

  @override
  PSNode token;

  @override
  PSEntry? url;

  _PSHost(this.token, {required this.isShortForm});

  @override
  String toString() => '''
    $token:
      $name
      $url''';
}

class _PSNode implements PSNode {
  @override
  final String? text;
  @override
  final SourceSpan span;

  final ResourceProvider? resourceProvider;

  _PSNode(YamlScalar node, this.resourceProvider)
      : text = node.value?.toString(),
        span = node.span;

  @override
  Source get source {
    var provider = resourceProvider ?? PhysicalResourceProvider.INSTANCE;
    var uri = span.sourceUrl!;
    var filePath = provider.pathContext.fromUri(uri);
    var file = provider.getFile(filePath);
    return FileSource(file, uri);
  }

  @override
  String toString() => '$text';
}

class _PSNodeList extends PSNodeList {
  @override
  final PSNode token;
  final Iterable<PSNode> nodes;

  _PSNodeList(this.token, this.nodes);

  @override
  Iterator<PSNode> get iterator => nodes.iterator;

  @override
  String toString() => '''
$token:
  - ${nodes.join('\n  - ')}''';
}

class _Pubspec implements Pubspec {
  @override
  PSEntry? author;
  @override
  PSNodeList? authors;
  @override
  PSEntry? description;
  @override
  PSEntry? documentation;
  @override
  PSEnvironment? environment;
  @override
  PSEntry? homepage;
  @override
  PSEntry? issueTracker;
  @override
  PSEntry? name;
  @override
  PSEntry? repository;
  @override
  PSEntry? version;
  @override
  PSDependencyList? dependencies;
  @override
  PSDependencyList? devDependencies;
  @override
  PSDependencyList? dependencyOverrides;

  _Pubspec(YamlNode yaml, {ResourceProvider? resourceProvider}) {
    try {
      _parse(yaml, resourceProvider: resourceProvider);
    } on Exception {
      // ignore
    }
  }

  @override
  void accept(PubspecVisitor visitor) {
    if (author != null) {
      visitor.visitPackageAuthor(author!);
    }
    if (authors != null) {
      visitor.visitPackageAuthors(authors!);
    }
    if (description != null) {
      visitor.visitPackageDescription(description!);
    }
    if (documentation != null) {
      visitor.visitPackageDocumentation(documentation!);
    }
    if (environment != null) {
      visitor.visitPackageEnvironment(environment!);
    }
    if (homepage != null) {
      visitor.visitPackageHomepage(homepage!);
    }
    if (issueTracker != null) {
      visitor.visitPackageIssueTracker(issueTracker!);
    }
    if (repository != null) {
      visitor.visitPackageRepository(repository!);
    }
    if (name != null) {
      visitor.visitPackageName(name!);
    }
    if (version != null) {
      visitor.visitPackageVersion(version!);
    }
    if (dependencies != null) {
      visitor.visitPackageDependencies(dependencies!);
      dependencies!.forEach(visitor.visitPackageDependency);
    }
    if (devDependencies != null) {
      visitor.visitPackageDevDependencies(devDependencies!);
      devDependencies!.forEach(visitor.visitPackageDevDependency);
    }
    if (dependencyOverrides != null) {
      visitor.visitPackageDependencyOverrides(dependencyOverrides!);
      dependencyOverrides!.forEach(visitor.visitPackageDependencyOverride);
    }
  }

  @override
  String toString() {
    var sb = _StringBuilder();
    sb.writelin(name);
    sb.writelin(version);
    sb.writelin(author);
    sb.writelin(authors);
    sb.writelin(description);
    sb.writelin(homepage);
    sb.writelin(repository);
    sb.writelin(issueTracker);
    sb.writelin(dependencies);
    sb.writelin(devDependencies);
    sb.writelin(dependencyOverrides);
    return sb.toString();
  }

  void _parse(YamlNode yaml, {ResourceProvider? resourceProvider}) {
    if (yaml is! YamlMap) {
      return;
    }

    yaml.nodes.forEach((k, v) {
      if (k is! YamlScalar) {
        return;
      }
      YamlScalar key = k;
      switch (key.toString()) {
        case 'author':
          author = _processScalar(key, v, resourceProvider);
        case 'authors':
          authors = _processScalarList(key, v, resourceProvider);
        case 'homepage':
          homepage = _processScalar(key, v, resourceProvider);
        case 'repository':
          repository = _processScalar(key, v, resourceProvider);
        case 'issue_tracker':
          issueTracker = _processScalar(key, v, resourceProvider);
        case 'name':
          name = _processScalar(key, v, resourceProvider);
        case 'description':
          description = _processScalar(key, v, resourceProvider);
        case 'documentation':
          documentation = _processScalar(key, v, resourceProvider);
        case 'dependencies':
          dependencies = _processDependencies(key, v, resourceProvider);
        case 'dev_dependencies':
          devDependencies = _processDependencies(key, v, resourceProvider);
        case 'dependency_overrides':
          dependencyOverrides = _processDependencies(key, v, resourceProvider);
        case 'environment':
          environment = _processEnvironment(key, v, resourceProvider);
        case 'version':
          version = _processScalar(key, v, resourceProvider);
      }
    });
  }
}

class _StringBuilder {
  StringBuffer buffer = StringBuffer();

  @override
  String toString() => buffer.toString();

  void writelin(Object? value) {
    if (value != null) {
      buffer.writeln(value);
    }
  }
}
