// Copyright 2014 The Flutter 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:core' hide print;
import 'dart:io' as system show exit;
import 'dart:io' hide exit;
import 'dart:math' as math;

import 'package:meta/meta.dart';

const Duration _quietTimeout = Duration(minutes: 10); // how long the output should be hidden between calls to printProgress before just being verbose

// If running from LUCI set to False.
final bool isLuci =  Platform.environment['LUCI_CI'] == 'True';
final bool hasColor = stdout.supportsAnsiEscapes && !isLuci;


final String bold = hasColor ? '\x1B[1m' : ''; // shard titles
final String red = hasColor ? '\x1B[31m' : ''; // errors
final String green = hasColor ? '\x1B[32m' : ''; // section titles, commands
final String yellow = hasColor ? '\x1B[33m' : ''; // indications that a test was skipped (usually renders orange or brown)
final String cyan = hasColor ? '\x1B[36m' : ''; // paths
final String reverse = hasColor ? '\x1B[7m' : ''; // clocks
final String gray = hasColor ? '\x1B[30m' : ''; // subtle decorative items (usually renders as dark gray)
final String white = hasColor ? '\x1B[37m' : ''; // last log line (usually renders as light gray)
final String reset = hasColor ? '\x1B[0m' : '';

const int kESC = 0x1B;
const int kOpenSquareBracket = 0x5B;
const int kCSIParameterRangeStart = 0x30;
const int kCSIParameterRangeEnd = 0x3F;
const int kCSIIntermediateRangeStart = 0x20;
const int kCSIIntermediateRangeEnd = 0x2F;
const int kCSIFinalRangeStart = 0x40;
const int kCSIFinalRangeEnd = 0x7E;


String get redLine {
  if (hasColor) {
    return '$red${'━' * stdout.terminalColumns}$reset';
  }
  return '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━';
}

String get clock {
  final DateTime now = DateTime.now();
  return '$reverse▌'
         '${now.hour.toString().padLeft(2, "0")}:'
         '${now.minute.toString().padLeft(2, "0")}:'
         '${now.second.toString().padLeft(2, "0")}'
         '▐$reset';
}

String prettyPrintDuration(Duration duration) {
  String result = '';
  final int minutes = duration.inMinutes;
  if (minutes > 0) {
    result += '${minutes}min ';
  }
  final int seconds = duration.inSeconds - minutes * 60;
  final int milliseconds = duration.inMilliseconds - (seconds * 1000 + minutes * 60 * 1000);
  result += '$seconds.${milliseconds.toString().padLeft(3, "0")}s';
  return result;
}

typedef PrintCallback = void Function(Object? line);
typedef VoidCallback = void Function();

// Allow print() to be overridden, for tests.
//
// Files that import this library should not import `print` from dart:core
// and should not use dart:io's `stdout` or `stderr`.
//
// By default this hides log lines between `printProgress` calls unless a
// timeout expires or anything calls `foundError`.
//
// Also used to implement `--verbose` in test.dart.
PrintCallback print = _printQuietly;

// Called by foundError and used to implement `--abort-on-error` in test.dart.
VoidCallback? onError;

bool get hasError => _hasError;
bool _hasError = false;

List<List<String>> _errorMessages = <List<String>>[];

final List<String> _pendingLogs = <String>[];
Timer? _hideTimer; // When this is null, the output is verbose.

void foundError(List<String> messages) {
  assert(messages.isNotEmpty);
  // Make the error message easy to notice in the logs by
  // wrapping it in a red box.
  final int width = math.max(15, (hasColor ? stdout.terminalColumns : 80) - 1);
  final String title = 'ERROR #${_errorMessages.length + 1}';
  print('$red╔═╡$bold$title$reset$red╞═${"═" * (width - 4 - title.length)}');
  for (final String message in messages.expand((String line) => line.split('\n'))) {
    print('$red║$reset $message');
  }
  print('$red╚${"═" * width}');
  // Normally, "print" actually prints to the log. To make the errors visible,
  // and to include useful context, print the entire log up to this point, and
  // clear it. Subsequent messages will continue to not be logged until there is
  // another error.
  _pendingLogs.forEach(_printLoudly);
  _pendingLogs.clear();
  _errorMessages.add(messages);
  _hasError = true;
  onError?.call();
}

@visibleForTesting
void resetErrorStatus() {
  _hasError = false;
  _errorMessages.clear();
  _pendingLogs.clear();
  _hideTimer?.cancel();
  _hideTimer = null;
}

Never reportSuccessAndExit(String message) {
  _hideTimer?.cancel();
  _hideTimer = null;
  print('$clock $message$reset');
  system.exit(0);
}

Never reportErrorsAndExit(String message) {
  _hideTimer?.cancel();
  _hideTimer = null;
  print('$clock $message$reset');
  print(redLine);
  print('${red}For your convenience, the error messages reported above are repeated here:$reset');
  final bool printSeparators = _errorMessages.any((List<String> messages) => messages.length > 1);
  if (printSeparators) {
    print('  🙙  🙛  ');
  }
  for (int index = 0; index < _errorMessages.length * 2 - 1; index += 1) {
    if (index.isEven) {
      _errorMessages[index ~/ 2].forEach(print);
    } else if (printSeparators) {
      print('  🙙  🙛  ');
    }
  }
  print(redLine);
  print('You may find the errors by searching for "╡ERROR #" in the logs.');
  system.exit(1);
}

void printProgress(String message) {
  _pendingLogs.clear();
  _hideTimer?.cancel();
  _hideTimer = null;
  print('$clock $message$reset');
  if (hasColor) {
    // This sets up a timer to switch to verbose mode when the tests take too long,
    // so that if a test hangs we can see the logs.
    // (This is only supported with a color terminal. When the terminal doesn't
    // support colors, the scripts just print everything verbosely, that way in
    // CI there's nothing hidden.)
    _hideTimer = Timer(_quietTimeout, () {
      _hideTimer = null;
      _pendingLogs.forEach(_printLoudly);
      _pendingLogs.clear();
    });
  }
}

final Pattern _lineBreak = RegExp(r'[\r\n]');

void _printQuietly(Object? message) {
  // The point of this function is to avoid printing its output unless the timer
  // has gone off in which case the function assumes verbose mode is active and
  // prints everything. To show that progress is still happening though, rather
  // than showing nothing at all, it instead shows the last line of output and
  // keeps overwriting it. To do this in color mode, carefully measures the line
  // of text ignoring color codes, which is what the parser below does.
  if (_hideTimer != null) {
    _pendingLogs.add(message.toString());
    String line = '$message'.trimRight();
    final int start = line.lastIndexOf(_lineBreak) + 1;
    int index = start;
    int length = 0;
    while (index < line.length && length < stdout.terminalColumns) {
      if (line.codeUnitAt(index) == kESC) { // 0x1B
        index += 1;
        if (index < line.length && line.codeUnitAt(index) == kOpenSquareBracket) { // 0x5B, [
          // That was the start of a CSI sequence.
          index += 1;
          while (index < line.length && line.codeUnitAt(index) >= kCSIParameterRangeStart
                                     && line.codeUnitAt(index) <= kCSIParameterRangeEnd) { // 0x30..0x3F
            index += 1; // ...parameter bytes...
          }
          while (index < line.length && line.codeUnitAt(index) >= kCSIIntermediateRangeStart
                                     && line.codeUnitAt(index) <= kCSIIntermediateRangeEnd) { // 0x20..0x2F
            index += 1; // ...intermediate bytes...
          }
          if (index < line.length && line.codeUnitAt(index) >= kCSIFinalRangeStart
                                  && line.codeUnitAt(index) <= kCSIFinalRangeEnd) { // 0x40..0x7E
            index += 1; // ...final byte.
          }
        }
      } else {
        index += 1;
        length += 1;
      }
    }
    line = line.substring(start, index);
    if (line.isNotEmpty) {
      stdout.write('\r\x1B[2K$white$line$reset');
    }
  } else {
    _printLoudly('$message');
  }
}

void _printLoudly(String message) {
  if (hasColor) {
    // Overwrite the last line written by _printQuietly.
    stdout.writeln('\r\x1B[2K$reset${message.trimRight()}');
  } else {
    stdout.writeln(message);
  }
}

// THE FOLLOWING CODE IS A VIOLATION OF OUR STYLE GUIDE
// BECAUSE IT INTRODUCES A VERY FLAKY RACE CONDITION
// https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#never-check-if-a-port-is-available-before-using-it-never-add-timeouts-and-other-race-conditions
// DO NOT USE THE FOLLOWING FUNCTIONS
// DO NOT WRITE CODE LIKE THE FOLLOWING FUNCTIONS
// https://github.com/flutter/flutter/issues/109474

int _portCounter = 8080;

/// Finds the next available local port.
Future<int> findAvailablePortAndPossiblyCauseFlakyTests() async {
  while (!await _isPortAvailable(_portCounter)) {
    _portCounter += 1;
  }
  return _portCounter++;
}

Future<bool> _isPortAvailable(int port) async {
  try {
    final RawSocket socket = await RawSocket.connect('localhost', port);
    socket.shutdown(SocketDirection.both);
    await socket.close();
    return false;
  } on SocketException {
    return true;
  }
}