// Copyright (c) 2019, 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.

// dart:io dependent functionality for reading and writing configuration files.

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

import 'errors.dart';
import 'package_config_impl.dart';
import 'package_config_json.dart';
import 'packages_file.dart' as packages_file;
import 'util.dart';
import 'util_io.dart';

/// Name of directory where Dart tools store their configuration.
///
/// Directory is created in the package root directory.
const dartToolDirName = '.dart_tool';

/// Name of file containing new package configuration data.
///
/// File is stored in the dart tool directory.
const packageConfigFileName = 'package_config.json';

/// Name of file containing legacy package configuration data.
///
/// File is stored in the package root directory.
const packagesFileName = '.packages';

/// Reads a package configuration file.
///
/// Detects whether the [file] is a version one `.packages` file or
/// a version two `package_config.json` file.
///
/// If the [file] is a `.packages` file and [preferNewest] is true,
/// first checks whether there is an adjacent `.dart_tool/package_config.json`
/// file, and if so, reads that instead.
/// If [preferNewest] is false, the specified file is loaded even if it is
/// a `.packages` file and there is an available `package_config.json` file.
///
/// The file must exist and be a normal file.
Future<PackageConfig> readAnyConfigFile(
    File file, bool preferNewest, void Function(Object error) onError) async {
  if (preferNewest && fileName(file.path) == packagesFileName) {
    var alternateFile = File(
        pathJoin(dirName(file.path), dartToolDirName, packageConfigFileName));
    if (alternateFile.existsSync()) {
      return await readPackageConfigJsonFile(alternateFile, onError);
    }
  }
  Uint8List bytes;
  try {
    bytes = await file.readAsBytes();
  } catch (e) {
    onError(e);
    return const SimplePackageConfig.empty();
  }
  return parseAnyConfigFile(bytes, file.uri, onError);
}

/// Like [readAnyConfigFile] but uses a URI and an optional loader.
Future<PackageConfig> readAnyConfigFileUri(
    Uri file,
    Future<Uint8List?> Function(Uri uri)? loader,
    void Function(Object error) onError,
    bool preferNewest) async {
  if (file.isScheme('package')) {
    throw PackageConfigArgumentError(
        file, 'file', 'Must not be a package: URI');
  }
  if (loader == null) {
    if (file.isScheme('file')) {
      return await readAnyConfigFile(File.fromUri(file), preferNewest, onError);
    }
    loader = defaultLoader;
  }
  if (preferNewest && file.pathSegments.last == packagesFileName) {
    var alternateFile = file.resolve('$dartToolDirName/$packageConfigFileName');
    Uint8List? bytes;
    try {
      bytes = await loader(alternateFile);
    } catch (e) {
      onError(e);
      return const SimplePackageConfig.empty();
    }
    if (bytes != null) {
      return parsePackageConfigBytes(bytes, alternateFile, onError);
    }
  }
  Uint8List? bytes;
  try {
    bytes = await loader(file);
  } catch (e) {
    onError(e);
    return const SimplePackageConfig.empty();
  }
  if (bytes == null) {
    onError(PackageConfigArgumentError(
        file.toString(), 'file', 'File cannot be read'));
    return const SimplePackageConfig.empty();
  }
  return parseAnyConfigFile(bytes, file, onError);
}

/// Parses a `.packages` or `package_config.json` file's contents.
///
/// Assumes it's a JSON file if the first non-whitespace character
/// is `{`, otherwise assumes it's a `.packages` file.
PackageConfig parseAnyConfigFile(
    Uint8List bytes, Uri file, void Function(Object error) onError) {
  var firstChar = firstNonWhitespaceChar(bytes);
  if (firstChar != $lbrace) {
    // Definitely not a JSON object, probably a .packages.
    return packages_file.parse(bytes, file, onError);
  }
  return parsePackageConfigBytes(bytes, file, onError);
}

Future<PackageConfig> readPackageConfigJsonFile(
    File file, void Function(Object error) onError) async {
  Uint8List bytes;
  try {
    bytes = await file.readAsBytes();
  } catch (error) {
    onError(error);
    return const SimplePackageConfig.empty();
  }
  return parsePackageConfigBytes(bytes, file.uri, onError);
}

Future<PackageConfig> readDotPackagesFile(
    File file, void Function(Object error) onError) async {
  Uint8List bytes;
  try {
    bytes = await file.readAsBytes();
  } catch (error) {
    onError(error);
    return const SimplePackageConfig.empty();
  }
  return packages_file.parse(bytes, file.uri, onError);
}

Future<void> writePackageConfigJsonFile(
    PackageConfig config, Directory targetDirectory) async {
  // Write .dart_tool/package_config.json first.
  var dartToolDir = Directory(pathJoin(targetDirectory.path, dartToolDirName));
  await dartToolDir.create(recursive: true);
  var file = File(pathJoin(dartToolDir.path, packageConfigFileName));
  var baseUri = file.uri;

  var sink = file.openWrite(encoding: utf8);
  writePackageConfigJsonUtf8(config, baseUri, sink);
  var doneJson = sink.close();

  // Write .packages too.
  file = File(pathJoin(targetDirectory.path, packagesFileName));
  baseUri = file.uri;
  sink = file.openWrite(encoding: utf8);
  writeDotPackages(config, baseUri, sink);
  var donePackages = sink.close();

  await Future.wait([doneJson, donePackages]);
}
