// Copyright 2013 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:convert';
import 'dart:io';

import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

// From errno definitions.
const int _noSuchFileError = 2;

/// An override function used by the tests to override the environment variable
/// lookups using [xdgEnvironmentOverride].
typedef EnvironmentAccessor = String? Function(String envVar);

/// A testing setter that replaces the real environment lookups with an override.
///
/// Set to null to stop overriding.
///
/// Only available to tests.
@visibleForTesting
set xdgEnvironmentOverride(EnvironmentAccessor? override) {
  _xdgEnvironmentOverride = override;
  _getenv = _xdgEnvironmentOverride ?? _productionGetEnv;
}

/// A testing getter that returns the current value of the override that
/// replaces the real environment lookups with an override.
///
/// Only available to tests.
@visibleForTesting
EnvironmentAccessor? get xdgEnvironmentOverride => _xdgEnvironmentOverride;
EnvironmentAccessor? _xdgEnvironmentOverride;
EnvironmentAccessor _getenv = _productionGetEnv;
String? _productionGetEnv(String value) => Platform.environment[value];

/// A wrapper around Process.runSync to allow injection of a fake in tests.
@visibleForTesting
abstract class XdgProcessRunner {
  /// Runs the given command synchronously.
  ProcessResult runSync(
    String executable,
    List<String> arguments, {
    Encoding? stdoutEncoding = systemEncoding,
    Encoding? stderrEncoding = systemEncoding,
  });
}

class _DefaultProcessRunner implements XdgProcessRunner {
  const _DefaultProcessRunner();

  @override
  ProcessResult runSync(String executable, List<String> arguments,
      {Encoding? stdoutEncoding = systemEncoding,
      Encoding? stderrEncoding = systemEncoding}) {
    return Process.runSync(
      executable,
      arguments,
      stdoutEncoding: stdoutEncoding,
      stderrEncoding: stderrEncoding,
    );
  }
}

/// A testing function that replaces the process runner used to run
/// xdg-user-path with the one supplied.
///
/// Only available to tests.
@visibleForTesting
set xdgProcessRunner(XdgProcessRunner processRunner) {
  _processRunner = processRunner;
}

XdgProcessRunner _processRunner = const _DefaultProcessRunner();

List<Directory> _directoryListFromEnvironment(
    String envVar, List<Directory> fallback) {
  ArgumentError.checkNotNull(envVar);
  ArgumentError.checkNotNull(fallback);
  final String? value = _getenv(envVar);
  if (value == null || value.isEmpty) {
    return fallback;
  }
  return value.split(':').where((String value) {
    return value.isNotEmpty;
  }).map<Directory>((String entry) {
    return Directory(entry);
  }).toList();
}

Directory? _directoryFromEnvironment(String envVar) {
  ArgumentError.checkNotNull(envVar);
  final String? value = _getenv(envVar);
  if (value == null || value.isEmpty) {
    return null;
  }
  return Directory(value);
}

Directory _directoryFromEnvironmentWithFallback(
    String envVar, String fallback) {
  ArgumentError.checkNotNull(envVar);
  final String? value = _getenv(envVar);
  if (value == null || value.isEmpty) {
    return _getDirectory(fallback);
  }
  return Directory(value);
}

// Creates a Directory from a fallback path.
Directory _getDirectory(String subdir) {
  ArgumentError.checkNotNull(subdir);
  assert(subdir.isNotEmpty);
  final String? homeDir = _getenv('HOME');
  if (homeDir == null || homeDir.isEmpty) {
    throw StateError(
        'The "HOME" environment variable is not set. This package (and POSIX) '
        'requires that HOME be set.');
  }
  return Directory(path.joinAll(<String>[homeDir, subdir]));
}

/// The base directory relative to which user-specific
/// non-essential (cached) data should be written. (Corresponds to
/// `$XDG_CACHE_HOME`).
///
/// Throws [StateError] if the HOME environment variable is not set.
Directory get cacheHome =>
    _directoryFromEnvironmentWithFallback('XDG_CACHE_HOME', '.cache');

/// The list of preference-ordered base directories relative to
/// which configuration files should be searched. (Corresponds to
/// `$XDG_CONFIG_DIRS`).
///
/// Throws [StateError] if the HOME environment variable is not set.
List<Directory> get configDirs {
  return _directoryListFromEnvironment(
    'XDG_CONFIG_DIRS',
    <Directory>[Directory('/etc/xdg')],
  );
}

/// The a single base directory relative to which user-specific
/// configuration files should be written. (Corresponds to `$XDG_CONFIG_HOME`).
///
/// Throws [StateError] if the HOME environment variable is not set.
Directory get configHome =>
    _directoryFromEnvironmentWithFallback('XDG_CONFIG_HOME', '.config');

/// The list of preference-ordered base directories relative to
/// which data files should be searched. (Corresponds to `$XDG_DATA_DIRS`).
///
/// Throws [StateError] if the HOME environment variable is not set.
List<Directory> get dataDirs {
  return _directoryListFromEnvironment(
    'XDG_DATA_DIRS',
    <Directory>[Directory('/usr/local/share'), Directory('/usr/share')],
  );
}

/// The base directory relative to which user-specific data files should be
/// written. (Corresponds to `$XDG_DATA_HOME`).
///
/// Throws [StateError] if the HOME environment variable is not set.
Directory get dataHome =>
    _directoryFromEnvironmentWithFallback('XDG_DATA_HOME', '.local/share');

/// The base directory relative to which user-specific runtime
/// files and other file objects should be placed. (Corresponds to
/// `$XDG_RUNTIME_DIR`).
///
/// Throws [StateError] if the HOME environment variable is not set.
Directory? get runtimeDir => _directoryFromEnvironment('XDG_RUNTIME_DIR');

/// The base directory relative to which user-specific state data should be
/// written. (Corresponds to `$XDG_STATE_HOME`).
///
/// Throws [StateError] if the HOME environment variable is not set.
Directory get stateHome =>
    _directoryFromEnvironmentWithFallback('XDG_STATE_HOME', '.local/state');

/// Gets the xdg user directory named by `dirName`.
///
/// Use [getUserDirectoryNames] to find out the list of available names.
///
/// If the `xdg-user-dir` executable is not present this returns null.
Directory? getUserDirectory(String dirName) {
  final ProcessResult result;
  try {
    result = _processRunner.runSync(
      'xdg-user-dir',
      <String>[dirName],
      stdoutEncoding: utf8,
    );
  } on ProcessException catch (e) {
    // Silently return null if it's missing, otherwise pass the exception up.
    if (e.errorCode == _noSuchFileError) {
      return null;
    }
    rethrow;
  }
  final String path = (result.stdout as String).split('\n')[0];
  return Directory(path);
}

/// Gets the set of user directory names that xdg knows about.
///
/// These are not paths, they are names of xdg values.  Call [getUserDirectory]
/// to get the associated directory.
///
/// These are the names of the variables in "[configHome]/user-dirs.dirs", with
/// the `XDG_` prefix removed and the `_DIR` suffix removed.
Set<String> getUserDirectoryNames() {
  final File configFile = File(path.join(configHome.path, 'user-dirs.dirs'));
  List<String> contents;
  try {
    contents = configFile.readAsLinesSync();
  } on FileSystemException {
    return const <String>{};
  }
  final Set<String> result = <String>{};
  final RegExp dirRegExp =
      RegExp(r'^\s*XDG_(?<dirname>[^=]*)_DIR\s*=\s*(?<dir>.*)\s*$');
  for (final String line in contents) {
    final RegExpMatch? match = dirRegExp.firstMatch(line);
    if (match == null) {
      continue;
    }
    result.add(match.namedGroup('dirname')!);
  }
  return result;
}
