// Copyright (c) 2023, 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:convert' show jsonEncode;
import 'dart:io' show File, FileSystemEntityType, IOException;

import 'expect_json.dart';
import 'io.dart';

// TODO: Convert the 'rootUri' reference below to a doc comment reference once
// https://github.com/dart-lang/linter/issues/4645 is addressed.

/// Entry in the `.dart_tool/extension_discovery/<package>.json` file.
///
/// If the `rootUri` is not an absolute path, then we will assume that the
/// package is mutable (either it's the root package or a path dependency).
/// If there is no extension config file for a mutable package, then we will
/// still store a [RegistryEntry] with `config = null`. Because everytime we
/// load the registry, we still need to check if a configuration file has been
/// added to the mutable package.
typedef RegistryEntry = ({
  String package,
  Uri rootUri,
  Uri packageUri,
  Map<String, Object?>? config,
});

typedef Registry = List<RegistryEntry>;

Future<Registry?> loadRegistry(File registryFile) async {
  try {
    final registryJson = decodeJsonMap(await registryFile.readAsString());
    if (registryJson.expectNumber('version') != 2) {
      throw const FormatException('"version" must be 2');
    }
    return registryJson
        .expectListObjects('entries')
        .map((e) => (
              package: e.expectString('package'),
              rootUri: e.expectUri('rootUri'),
              packageUri: e.expectUri('packageUri'),
              config: e.optionalMap('config'),
            ))
        .toList(growable: false);
  } on IOException {
    return null; // pass
  } on FormatException {
    await registryFile.tryDelete();
    return null;
  }
}

Future<void> saveRegistry(
  File registryFile,
  Registry registry,
) async {
  try {
    if (!registryFile.parent.existsSync()) {
      await registryFile.parent.create();
    }
    final tmpFile = File('${registryFile.path}.tmp');
    final tmpFileStat = tmpFile.statSync();
    if (tmpFileStat.type != FileSystemEntityType.notFound) {
      final tmpAge = DateTime.now().difference(tmpFileStat.modified);
      if (tmpAge.inSeconds < 5) {
        // Don't try to write registry, if there is an abandoned temporary file
        // no older than 5 seconds. Otherwise, we could have race conditions!
        // Note: That saving the registry is a performance improvement, not a
        //       strict necessity!
        return;
      } else {
        await tmpFile.delete();
      }
    }

    await tmpFile.writeAsString(jsonEncode({
      'version': 2,
      'entries': registry
          .map((e) => {
                'package': e.package,
                'rootUri': e.rootUri.toString(),
                'packageUri': e.packageUri.toString(),
                if (e.config != null) 'config': e.config,
              })
          .toList(),
    }));
    await tmpFile.reliablyRename(registryFile.path);
    await _ensureReadme(
      File.fromUri(registryFile.parent.uri.resolve('README.md')),
    );
  } on IOException {
    // pass
  }
}

Future<void> _ensureReadme(File readmeFile) async {
  try {
    final stat = readmeFile.statSync();
    if (stat.type != FileSystemEntityType.notFound) {
      final age = DateTime.now().difference(stat.modified);
      if (age.inDays < 5) {
        return; // don't update README.md, if it's less than 5 days old
      }
    }
    await readmeFile.writeAsString(_readmeContents);
  } on IOException {
    // pass
  }
}

const _readmeContents = '''
Extension Discovery Cache
=========================

This folder is used by `package:extension_discovery` to cache lists of
packages that contains extensions for other packages.

DO NOT USE THIS FOLDER
----------------------

 * Do not read (or rely) the contents of this folder.
 * Do write to this folder.

If you're interested in the lists of extensions stored in this folder use the
API offered by package `extension_discovery` to get this information.

If this package doesn't work for your use-case, then don't try to read the
contents of this folder. It may change, and will not remain stable.

Use package `extension_discovery`
---------------------------------

If you want to access information from this folder.

Feel free to delete this folder
-------------------------------

Files in this folder act as a cache, and the cache is discarded if the files
are older than the modification time of `.dart_tool/package_config.json`.

Hence, it should never be necessary to clear this cache manually, if you find a
need to do please file a bug.
''';
