// Copyright 2024 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'dart:io';
import 'dart:convert';

import 'package:grinder/grinder.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';

/// A regular expression that matches a version in a pubspec.
final _pubspecVersionRegExp = RegExp(r'^version: (.*)$', multiLine: true);

/// A regular expression that matches a Sass dependency version in a pubspec.
final _sassVersionRegExp = RegExp(r'^( +)sass: (\d.*)$', multiLine: true);

/// A regular expression that matches a CHANGELOG header for a dev version
final _changelogDevHeaderRegExp = RegExp(r'^## .*-dev$', multiLine: true);

/// Adds grinder tasks for bumping package versions.
void addBumpVersionTasks() {
  for (var patch in [false, true]) {
    for (var dev in [true, false]) {
      addTask(
        GrinderTask(
          'bump-version-${patch ? 'patch' : 'minor'}' + (dev ? '-dev' : ''),
          taskFunction: () => _bumpVersion(patch, dev),
          description: 'Bump the version of all packages to the next '
              '${patch ? 'patch' : 'minor'}${dev ? ' dev' : ''} version',
        ),
      );
    }
  }
}

/// Bumps the current package versions to the next [patch] version, with `-dev`
/// if [dev] is true.
void _bumpVersion(bool patch, bool dev) {
  // Returns the version to which to bump [version].
  Version chooseNextVersion(Version version, SourceSpan span) {
    if (dev) {
      if (version.preRelease.isNotEmpty && (patch || version.patch == 0)) {
        fail(span.message("Version is already pre-release", color: true));
      }
    } else if (version.preRelease.length == 1 &&
        version.preRelease.first == "dev" &&
        (patch || version.patch == 0)) {
      // If it's already a dev version, just mark it stable instead of
      // increasing it.
      return Version(version.major, version.minor, version.patch);
    }

    var nextVersion =
        patch || version.major == 0 ? version.nextPatch : version.nextMinor;
    return Version(
      nextVersion.major,
      nextVersion.minor,
      nextVersion.patch,
      pre: dev ? "dev" : null,
    );
  }

  /// Adds a "No user-visible changes" entry for [version] to the changelog in
  /// [dir].
  void addChangelogEntry(String dir, Version version) {
    var path = p.join(dir, "CHANGELOG.md");
    var text = File(path).readAsStringSync();
    if (text.startsWith(_changelogDevHeaderRegExp)) {
      File(path).writeAsStringSync(
        text.replaceFirst(_changelogDevHeaderRegExp, "## $version"),
      );
    } else if (text.startsWith("## $version\n")) {
      return;
    } else {
      File(
        path,
      ).writeAsStringSync("## $version\n\n* No user-visible changes.\n\n$text");
    }
  }

  // Bumps the current version of [pubspec] to the next [patch] version, with
  // `-dev` if [dev] is true.
  //c
  // If [sassVersion] is passed, this bumps the `sass` dependency to that version.
  //
  // Returns the new version of this package.
  Version bumpDartVersion(String path, [Version? sassVersion]) {
    var text = File(path).readAsStringSync();
    var pubspec = loadYaml(text, sourceUrl: p.toUri(path)) as YamlMap;
    var version = chooseNextVersion(
      Version.parse(pubspec["version"] as String),
      pubspec.nodes["version"]!.span,
    );

    text = text.replaceFirst(_pubspecVersionRegExp, 'version: $version');
    if (sassVersion != null) {
      // Don't depend on a prerelease version, depend on its released
      // equivalent.
      var sassDependencyVersion = Version(
        sassVersion.major,
        sassVersion.minor,
        sassVersion.patch,
      );
      text = text.replaceFirstMapped(
        _sassVersionRegExp,
        (match) => '${match[1]}sass: $sassDependencyVersion',
      );
    }

    File(path).writeAsStringSync(text);
    addChangelogEntry(p.dirname(path), version);
    return version;
  }

  var sassVersion = bumpDartVersion('pubspec.yaml');
  bumpDartVersion('pkg/sass_api/pubspec.yaml', sassVersion);

  var packageJsonPath = 'pkg/sass-parser/package.json';
  var packageJsonText = File(packageJsonPath).readAsStringSync();
  var packageJson =
      loadYaml(packageJsonText, sourceUrl: p.toUri(packageJsonPath)) as YamlMap;
  var version = chooseNextVersion(
    Version.parse(packageJson["version"] as String),
    packageJson.nodes["version"]!.span,
  );
  File(packageJsonPath).writeAsStringSync(
    JsonEncoder.withIndent(
          "  ",
        ).convert({...packageJson, "version": version.toString()}) +
        "\n",
  );
  addChangelogEntry("pkg/sass-parser", version);
}
