// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Updates the version numbers of the Flutter repo.
// Only tested on Linux.
//
// See: https://github.com/flutter/flutter/wiki/Release-process

import 'dart:io';

import 'package:args/args.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

const String kIncrement = 'increment';
const String kBrokeSdk = 'broke-sdk';
const String kBrokeFramework = 'broke-framework';
const String kBrokeTest = 'broke-test';
const String kBrokeDriver = 'broke-driver';
const String kMarkRelease = 'release';
const String kHelp = 'help';

const String kYamlVersionPrefix = 'version: ';
const String kDev = '-dev';

enum VersionKind { dev, release }

void main(List<String> args) {
  // If we're run from the `tools` dir, set the cwd to the repo root.
  if (path.basename(Directory.current.path) == 'tools')
    Directory.current = Directory.current.parent.parent;

  final ArgParser argParser = new ArgParser();
  argParser.addFlag(kIncrement, defaultsTo: false, help: 'Increment all the version numbers. Cannot be specified with --$kMarkRelease or with any --broke-* commands.');
  argParser.addFlag(kBrokeSdk, defaultsTo: false, negatable: false, help: 'Increment the Flutter SDK version number to indicate that there has been a breaking change to the SDK (for example, to the command line options).');
  argParser.addFlag(kBrokeFramework, defaultsTo: false, negatable: false, help: 'Increment the "flutter" package version number to indicate that there has been a breaking change to the Flutter framework.');
  argParser.addFlag(kBrokeTest, defaultsTo: false, negatable: false, help: 'Increment the "flutter_test" package version number to indicate that there has been a breaking change to the test API framework.');
  argParser.addFlag(kBrokeDriver, defaultsTo: false, negatable: false, help: 'Increment the "flutter_driver" package version number to indicate that there has been a breaking change to the driver API framework.');
  argParser.addFlag(kMarkRelease, defaultsTo: false, help: 'Remove "-dev" from each version number. This is used when releasing. When not present, "-dev" is added to each version number. Cannot be specified with --$kIncrement or with any --broke-* commands.');
  argParser.addFlag(kHelp, negatable: false, help: 'Show this help message.');
  final ArgResults argResults = argParser.parse(args);

  final bool increment = argResults[kIncrement];
  final bool brokeSdk = argResults[kBrokeSdk];
  final bool brokeFramework = argResults[kBrokeFramework];
  final bool brokeTest = argResults[kBrokeTest];
  final bool brokeDriver = argResults[kBrokeDriver];
  final bool brokeAnything = brokeSdk || brokeFramework || brokeTest || brokeDriver;
  final VersionKind level = argResults[kMarkRelease] ? VersionKind.release : VersionKind.dev;
  final bool help = argResults[kHelp];

  if (help) {
    print('update_versions.dart - update version numbers of Flutter packages and SDK');
    print(argParser.usage);
    exit(0);
  }

  final bool release = level == VersionKind.release;
  if ((brokeAnything && release) || (brokeAnything && increment) || (release && increment)) {
    print('You can either increment all the version numbers (--$kIncrement), indicate that some packages have had breaking changes (--broke-*), or switch to release mode (--$kMarkRelease).');
    print('You cannot combine these, however.');
    exit(1);
  }

  final RawVersion sdk = new RawVersion('VERSION');
  final PubSpecVersion framework = new PubSpecVersion('packages/flutter/pubspec.yaml');
  final PubSpecVersion test = new PubSpecVersion('packages/flutter_test/pubspec.yaml');
  final PubSpecVersion driver = new PubSpecVersion('packages/flutter_driver/pubspec.yaml');

  if (increment || brokeAnything)
    sdk.increment(brokeAnything);
  sdk.setMode(level);

  if (increment || brokeFramework)
    framework.increment(brokeFramework);
  framework.setMode(level);

  if (increment || brokeTest)
    test.increment(brokeTest);
  test.setMode(level);

  if (increment || brokeDriver)
    driver.increment(brokeDriver);
  driver.setMode(level);

  sdk.write();
  framework.write();
  test.write();
  driver.write();

  print('Flutter SDK is now at version: $sdk');
  print('flutter package is now at version: $framework');
  print('flutter_test package is now at version: $test');
  print('flutter_driver package is now at version: $driver');

  if (release) {
    print('\nDuring the tagging step in the instructions, the commands will be:');
    print('git tag $sdk');
    print('git push upstream $sdk');
  }
}

abstract class Version {
  Version() {
    read();
  }

  @protected
  final List<int> version = <int>[];

  @protected
  VersionKind level;

  @protected
  bool dirty = false;

  @protected
  void read();

  void interpret(String value) {
    level = value.endsWith(kDev) ? VersionKind.dev : VersionKind.release;
    if (level == VersionKind.dev)
      value = value.substring(0, value.length - kDev.length);
    version.addAll(value.split('.').map<int>(int.parse));
  }

  void increment(bool breaking) {
    assert(version.length == 3);
    if (breaking) {
      version[1] += 1;
      version[2] = 0;
    } else {
      version[2] += 1;
    }
    dirty = true;
  }

  void setMode(VersionKind value) {
    if (value != level) {
      level = value;
      dirty = true;
    }
  }

  void write();

  @override
  String toString() => version.join('.') + (level == VersionKind.dev ? kDev : '');
}

class PubSpecVersion extends Version {
  PubSpecVersion(this.path);

  final String path;

  @override
  void read() {
    final List<String> lines = new File(path).readAsLinesSync();
    final String versionLine = lines.where((String line) => line.startsWith(kYamlVersionPrefix)).single;
    interpret(versionLine.substring(kYamlVersionPrefix.length));
  }

  @override
  void write() {
    if (!dirty)
      return;
    final List<String> lines = new File(path).readAsLinesSync();
    for (int index = 0; index < lines.length; index += 1) {
      final String line = lines[index];
      if (line.startsWith(kYamlVersionPrefix)) {
        lines[index] = '$kYamlVersionPrefix$this';
        break;
      }
    }
    new File(path).writeAsStringSync(lines.join('\n') + '\n');
  }
}

class RawVersion extends Version {
  RawVersion(this.path);

  final String path;

  @override
  void read() {
    final List<String> lines = new File(path).readAsLinesSync();
    interpret(lines.where((String line) => line.isNotEmpty && !line.startsWith('#')).single);
  }

  @override
  void write() {
    if (!dirty)
      return;
    final List<String> lines = new File(path).readAsLinesSync();
    for (int index = 0; index < lines.length; index += 1) {
      final String line = lines[index];
      if (line.isNotEmpty && !line.startsWith('#')) {
        lines[index] = '$this';
        break;
      }
    }
    new File(path).writeAsStringSync(lines.join('\n') + '\n');
  }
}
