// Copyright 2015 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.

import 'dart:async';
import 'dart:io';

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

import '../android/android_sdk.dart';
import '../base/context.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../cache.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import '../toolchain.dart';
import '../version.dart';

const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/
const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/
const String kFlutterEnginePackageName = 'sky_engine';

class FlutterCommandRunner extends CommandRunner {
  FlutterCommandRunner({ bool verboseHelp: false }) : super(
    'flutter',
    'Manage your Flutter app development.'
  ) {
    argParser.addFlag('verbose',
        abbr: 'v',
        negatable: false,
        help: 'Noisy logging, including all shell commands executed.');
    argParser.addFlag('quiet',
        negatable: false,
        hide: !verboseHelp,
        help: 'Reduce the amount of output from some commands.');
    argParser.addOption('device-id',
        abbr: 'd',
        help: 'Target device id or name (prefixes allowed).');
    argParser.addFlag('version',
        negatable: false,
        help: 'Reports the version of this tool.');
    argParser.addFlag('color',
        negatable: true,
        hide: !verboseHelp,
        help: 'Whether to use terminal colors.');
    argParser.addFlag('suppress-analytics',
        negatable: false,
        hide: !verboseHelp,
        help: 'Suppress analytics reporting when this command runs.');

    String packagesHelp;
    if (FileSystemEntity.isFileSync(kPackagesFileName))
      packagesHelp = '\n(defaults to "$kPackagesFileName")';
    else
      packagesHelp = '\n(required, since the current directory does not contain a "$kPackagesFileName" file)';
    argParser.addOption('packages',
        hide: !verboseHelp,
        help: 'Path to your ".packages" file.$packagesHelp');
    argParser.addOption('flutter-root',
        help: 'The root directory of the Flutter repository (uses \$$kFlutterRootEnvironmentVariableName if set).',
              defaultsTo: _defaultFlutterRoot);

    if (verboseHelp)
      argParser.addSeparator('Local build selection options (not normally required):');

    argParser.addOption('local-engine-src-path',
        hide: !verboseHelp,
        help:
            'Path to your engine src directory, if you are building Flutter locally.\n'
            'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to the path given in your pubspec.yaml\n'
            'dependency_overrides for $kFlutterEnginePackageName, if any, or, failing that, tries to guess at the location\n'
            'based on the value of the --flutter-root option.');

    argParser.addOption('local-engine',
        hide: !verboseHelp,
        help:
            'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
            'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
            'This path is relative to --local-engine-src-path/out.');
  }

  @override
  String get usageFooter {
    return 'Run "flutter -h -v" for verbose help output, including less commonly used options.';
  }

  static String get _defaultFlutterRoot {
    if (Platform.environment.containsKey(kFlutterRootEnvironmentVariableName))
      return Platform.environment[kFlutterRootEnvironmentVariableName];
    try {
      if (Platform.script.scheme == 'data')
        return '../..'; // we're running as a test
      String script = Platform.script.toFilePath();
      if (path.basename(script) == kSnapshotFileName)
        return path.dirname(path.dirname(path.dirname(script)));
      if (path.basename(script) == kFlutterToolsScriptFileName)
        return path.dirname(path.dirname(path.dirname(path.dirname(script))));

      // If run from a bare script within the repo.
      if (script.contains('flutter/packages/'))
        return script.substring(0, script.indexOf('flutter/packages/') + 8);
      if (script.contains('flutter/examples/'))
        return script.substring(0, script.indexOf('flutter/examples/') + 8);
    } catch (error) {
      // we don't have a logger at the time this is run
      // (which is why we don't use printTrace here)
      print('Unable to locate flutter root: $error');
    }
    return '.';
  }

  @override
  Future<dynamic> run(Iterable<String> args) {
    // Have an invocation of 'build' print out it's sub-commands.
    if (args.length == 1 && args.first == 'build')
      args = <String>['build', '-h'];

    return super.run(args).then((dynamic result) {
      return result;
    }).whenComplete(() {
      logger.flush();
    });
  }

  @override
  Future<int> runCommand(ArgResults globalResults) async {
    // Check for verbose.
    if (globalResults['verbose'])
      context[Logger] = new VerboseLogger();

    logger.quiet = globalResults['quiet'];

    if (globalResults.wasParsed('color'))
      logger.supportsColor = globalResults['color'];

    // We must set Cache.flutterRoot early because other features use it (e.g.
    // enginePath's initialiser uses it).
    Cache.flutterRoot = path.normalize(path.absolute(globalResults['flutter-root']));

    if (Platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
      await Cache.lock();

    if (globalResults['suppress-analytics'])
      flutterUsage.suppressAnalytics = true;

    if (!_checkFlutterCopy())
      return 1;

    if (globalResults.wasParsed('packages'))
      PackageMap.globalPackagesPath = path.normalize(path.absolute(globalResults['packages']));

    // See if the user specified a specific device.
    deviceManager.specifiedDeviceId = globalResults['device-id'];

    // Set up the tooling configuration.
    String enginePath = _findEnginePath(globalResults);
    if (enginePath != null) {
      ToolConfiguration.instance.engineSrcPath = enginePath;
      ToolConfiguration.instance.engineBuildPath = _findEngineBuildPath(globalResults, enginePath);
    }

    // The Android SDK could already have been set by tests.
    if (!context.isSet(AndroidSdk)) {
      if (enginePath != null) {
        context[AndroidSdk] = new AndroidSdk('$enginePath/third_party/android_tools/sdk');
      } else {
        context[AndroidSdk] = AndroidSdk.locateAndroidSdk();
      }
    }

    if (globalResults['version']) {
      flutterUsage.sendCommand('version');
      printStatus(FlutterVersion.getVersion(Cache.flutterRoot).toString());
      return 0;
    }

    return await super.runCommand(globalResults);
  }

  String _tryEnginePath(String enginePath) {
    if (FileSystemEntity.isDirectorySync(path.join(enginePath, 'out')))
      return enginePath;
    return null;
  }

  String _findEnginePath(ArgResults globalResults) {
    String engineSourcePath = globalResults['local-engine-src-path'] ?? Platform.environment[kFlutterEngineEnvironmentVariableName];

    if (engineSourcePath == null && globalResults['local-engine'] != null) {
      try {
        Uri engineUri = new PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName];
        engineSourcePath = path.dirname(path.dirname(path.dirname(path.dirname(engineUri.path))));
        bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out'));
        if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists)
          engineSourcePath = null;
      } on FileSystemException { } on FormatException { }

      if (engineSourcePath == null)
        engineSourcePath = _tryEnginePath(path.join(Cache.flutterRoot, '../engine/src'));

      if (engineSourcePath == null) {
        printError('Unable to detect local Flutter engine build directory.\n'
            'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n'
            'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n'
            'use --local-engine-src-path to specify the path to the root of your flutter/engine repository.');
        throw new ProcessExit(2);
      }
    }

    if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
      printError('Unable to detect a Flutter engine build directory in $engineSourcePath.\n'
          'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that\n'
          'you have compiled the engine in that directory, which should produce an \'out\' directory');
      throw new ProcessExit(2);
    }

    return engineSourcePath;
  }

  String _findEngineBuildPath(ArgResults globalResults, String enginePath) {
    String localEngine;
    if (globalResults['local-engine'] != null) {
      localEngine = globalResults['local-engine'];
    } else {
      printError('You must specify --local-engine if you are using a locally built engine.');
      throw new ProcessExit(2);
    }

    String engineBuildPath = path.normalize(path.join(enginePath, 'out', localEngine));
    if (!FileSystemEntity.isDirectorySync(engineBuildPath)) {
      printError('No Flutter engine build found at $engineBuildPath.');
      throw new ProcessExit(2);
    }

    return engineBuildPath;
  }

  static void initFlutterRoot() {
    if (Cache.flutterRoot == null)
      Cache.flutterRoot = _defaultFlutterRoot;
  }

  /// Get all pub packages in the Flutter repo.
  List<Directory> getRepoPackages() {
    return _gatherProjectPaths(path.absolute(Cache.flutterRoot))
      .map((String dir) => new Directory(dir))
      .toList();
  }

  static List<String> _gatherProjectPaths(String rootPath) {
    if (FileSystemEntity.isFileSync(path.join(rootPath, '.dartignore')))
      return <String>[];

    if (FileSystemEntity.isFileSync(path.join(rootPath, 'pubspec.yaml')))
      return <String>[rootPath];

    return new Directory(rootPath)
      .listSync(followLinks: false)
      .expand((FileSystemEntity entity) {
        return entity is Directory ? _gatherProjectPaths(entity.path) : <String>[];
      })
      .toList();
  }

  /// Get the entry-points we want to analyze in the Flutter repo.
  List<Directory> getRepoAnalysisEntryPoints() {
    final String rootPath = path.absolute(Cache.flutterRoot);
    final List<Directory> result = <Directory>[
      // not bin, and not the root
      new Directory(path.join(rootPath, 'dev')),
      new Directory(path.join(rootPath, 'examples')),
    ];
    // And since analyzer refuses to look at paths that end in "packages/":
    result.addAll(
      _gatherProjectPaths(path.join(rootPath, 'packages'))
      .map/*<Directory>*/((String path) => new Directory(path))
    );
    return result;
  }

  bool _checkFlutterCopy() {
    // If the current directory is contained by a flutter repo, check that it's
    // the same flutter that is currently running.
    String directory = path.normalize(path.absolute(Directory.current.path));

    // Check if the cwd is a flutter dir.
    while (directory.isNotEmpty) {
      if (_isDirectoryFlutterRepo(directory)) {
        if (!_compareResolvedPaths(directory, Cache.flutterRoot)) {
          printError(
            'Warning: the \'flutter\' tool you are currently running is not the one from the current directory:\n'
            '  running Flutter  : ${Cache.flutterRoot}\n'
            '  current directory: $directory\n'
            'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n'
            'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
          );
        }

        break;
      }

      String parent = path.dirname(directory);
      if (parent == directory)
        break;
      directory = parent;
    }

    // Check that the flutter running is that same as the one referenced in the pubspec.
    if (FileSystemEntity.isFileSync(kPackagesFileName)) {
      PackageMap packageMap = new PackageMap(kPackagesFileName);
      Uri flutterUri = packageMap.map['flutter'];

      if (flutterUri != null && (flutterUri.scheme == 'file' || flutterUri.scheme == '')) {
        // .../flutter/packages/flutter/lib
        Uri rootUri = flutterUri.resolve('../../..');
        String flutterPath = path.normalize(new File.fromUri(rootUri).absolute.path);

        if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) {
          printError(
            'Warning: the \'flutter\' tool you are currently running is different from the one referenced in your pubspec.yaml:\n'
            '  running Flutter  : ${Cache.flutterRoot}\n'
            '  pubspec reference: $flutterPath\n'
            'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n'
            'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path). You\n'
            'can also change which flutter your project points to by editing the \'flutter:\' path in your pubspec.yaml file.\n'
          );
        }
      }
    }

    return true;
  }

  // Check if `bin/flutter` and `bin/cache/engine.stamp` exist.
  bool _isDirectoryFlutterRepo(String directory) {
    return
      FileSystemEntity.isFileSync(path.join(directory, 'bin/flutter')) &&
      FileSystemEntity.isFileSync(path.join(directory, 'bin/cache/engine.stamp'));
  }
}

bool _compareResolvedPaths(String path1, String path2) {
  path1 = new Directory(path.absolute(path1)).resolveSymbolicLinksSync();
  path2 = new Directory(path.absolute(path2)).resolveSymbolicLinksSync();

  return path1 == path2;
}
