// Copyright (c) 2018, 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:async';
import 'dart:io';

import 'package:build/build.dart';
import 'package:logging/logging.dart';

import '../asset/batch.dart';
import '../asset/file_based.dart';
import '../asset/reader.dart';
import '../asset/writer.dart';
import '../generate/build_directory.dart';
import '../generate/build_result.dart';
import '../generate/finalized_assets_view.dart';
import '../package_graph/package_graph.dart';
import 'build_environment.dart';
import 'create_merged_dir.dart';

final _logger = Logger('IOEnvironment');

/// A [BuildEnvironment] writing to disk and stdout.
class IOEnvironment implements BuildEnvironment {
  @override
  final RunnerAssetReader reader;

  @override
  final RunnerAssetWriter writer;

  final bool _isInteractive;

  final bool _outputSymlinksOnly;

  final PackageGraph _packageGraph;

  IOEnvironment._(this.reader, this.writer, this._isInteractive,
      this._outputSymlinksOnly, this._packageGraph);

  factory IOEnvironment(
    PackageGraph packageGraph, {
    bool? assumeTty,
    bool outputSymlinksOnly = false,
    bool lowResourcesMode = false,
  }) {
    if (outputSymlinksOnly && Platform.isWindows) {
      _logger.warning('Symlinks to files are not yet working on Windows, you '
          'may experience issues using this mode. Follow '
          'https://github.com/dart-lang/sdk/issues/33966 for updates.');
    }

    var fileReader = FileBasedAssetReader(packageGraph);
    var fileWriter = FileBasedAssetWriter(packageGraph);

    var (reader, writer) = lowResourcesMode
        ? (fileReader, fileWriter)
        : wrapInBatch(
            reader: fileReader,
            pathProvidingReader: fileReader,
            writer: fileWriter);

    return IOEnvironment._(reader, writer, assumeTty == true || _canPrompt(),
        outputSymlinksOnly, packageGraph);
  }

  @override
  void onLog(LogRecord record) {
    if (record.level >= Level.SEVERE) {
      stderr.writeln(record);
    } else {
      stdout.writeln(record);
    }
  }

  @override
  Future<int> prompt(String message, List<String> choices) async {
    if (!_isInteractive) throw NonInteractiveBuildException();
    while (true) {
      stdout.writeln('\n$message');
      for (var i = 0, l = choices.length; i < l; i++) {
        stdout.writeln('${i + 1} - ${choices[i]}');
      }
      final input = stdin.readLineSync()!;
      final choice = int.tryParse(input) ?? -1;
      if (choice > 0 && choice <= choices.length) return choice - 1;
      stdout.writeln('Unrecognized option $input, '
          'a number between 1 and ${choices.length} expected');
    }
  }

  @override
  Future<BuildResult> finalizeBuild(
      BuildResult buildResult,
      FinalizedAssetsView finalizedAssetsView,
      AssetReader reader,
      Set<BuildDirectory> buildDirs) async {
    if (buildDirs
            .any((target) => target.outputLocation?.path.isNotEmpty ?? false) &&
        buildResult.status == BuildStatus.success) {
      if (!await createMergedOutputDirectories(buildDirs, _packageGraph, this,
          reader, finalizedAssetsView, _outputSymlinksOnly)) {
        return _convertToFailure(buildResult,
            failureType: FailureType.cantCreate);
      }
    }
    return buildResult;
  }
}

bool _canPrompt() =>
    stdioType(stdin) == StdioType.terminal &&
    // Assume running inside a test if the code is running as a `data:` URI
    Platform.script.scheme != 'data';

BuildResult _convertToFailure(BuildResult previous,
        {FailureType? failureType}) =>
    BuildResult(
      BuildStatus.failure,
      previous.outputs,
      performance: previous.performance,
      failureType: failureType,
    );
