// Copyright (c) 2014, 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:convert' show JsonEncoder, jsonDecode;
import 'dart:io';

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

import 'usage_impl.dart';

/// An interface to a Google Analytics session, suitable for use in command-line
/// applications.
///
/// [analyticsUrl] is an optional replacement for the default Google Analytics
/// URL (`https://www.google-analytics.com/collect`).
///
/// `trackingId`, `applicationName`, and `applicationVersion` values should be
/// supplied. `analyticsUrl` is optional, and lets user's substitute their own
/// analytics URL for the default.
///
/// `documentDirectory` is where the analytics settings are stored. It
/// defaults to the user home directory. For regular `dart:io` apps this doesn't
/// need to be supplied. For Flutter applications, you should pass in a value
/// like `PathProvider.getApplicationDocumentsDirectory()`.
///
/// [batchingDelay] is used to control batching behaviour. Events will be sent
/// batches of 20 after the duration is over from when the first message was
/// sent.
///
/// If [batchingDelay] is `Duration()` messages will be sent when control
/// returns to the event loop.
///
/// Batched messages are sent in batches of up to 20 messages. They will be sent
/// to [analyticsBatchingUrl] defaulting to
/// `https://www.google-analytics.com/batch`.
class AnalyticsIO extends AnalyticsImpl {
  AnalyticsIO(
    String trackingId,
    String applicationName,
    String applicationVersion, {
    String? analyticsUrl,
    String? analyticsBatchingUrl,
    Directory? documentDirectory,
    HttpClient? client,
    Duration? batchingDelay,
  }) : super(
          trackingId,
          IOPersistentProperties(applicationName,
              documentDirPath: documentDirectory?.path),
          IOPostHandler(client: client),
          applicationName: applicationName,
          applicationVersion: applicationVersion,
          analyticsUrl: analyticsUrl,
          analyticsBatchingUrl: analyticsBatchingUrl,
          batchingDelay: batchingDelay,
        ) {
    final locale = getPlatformLocale();
    if (locale != null) {
      setSessionValue('ul', locale);
    }
  }
}

@visibleForTesting
String createUserAgent() {
  final locale = getPlatformLocale() ?? '';

  if (Platform.isAndroid) {
    return 'Mozilla/5.0 (Android; Mobile; $locale)';
  } else if (Platform.isIOS) {
    return 'Mozilla/5.0 (iPhone; U; CPU iPhone OS like Mac OS X; $locale)';
  } else if (Platform.isMacOS) {
    return 'Mozilla/5.0 (Macintosh; Intel Mac OS X; Macintosh; $locale)';
  } else if (Platform.isWindows) {
    return 'Mozilla/5.0 (Windows; Windows; Windows; $locale)';
  } else if (Platform.isLinux) {
    return 'Mozilla/5.0 (Linux; Linux; Linux; $locale)';
  } else {
    // Dart/1.8.0 (macos; macos; macos; en_US)
    var os = Platform.operatingSystem;
    return 'Dart/${getDartVersion()} ($os; $os; $os; $locale)';
  }
}

String userHomeDir() {
  var envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
  var value = Platform.environment[envKey];
  return value ?? '.';
}

String getDartVersion() {
  var ver = Platform.version;
  var index = ver.indexOf(' ');
  if (index != -1) ver = ver.substring(0, index);
  return ver;
}

class IOPostHandler extends PostHandler {
  final HttpClient _client;

  IOPostHandler({HttpClient? client})
      : _client = (client ?? HttpClient())..userAgent = createUserAgent();

  @override
  String encodeHit(Map<String, dynamic> hit) {
    return postEncode(hit);
  }

  @override
  Future<void> sendPost(String url, List<String> batch) async {
    var data = batch.join('\n');
    try {
      var req = await _client.postUrl(Uri.parse(url));
      req.write(data);
      var response = await req.close();
      await response.drain<void>();
    } on Exception {
      // Catch errors that can happen during a request, but that we can't do
      // anything about, e.g. a missing internet connection.
    }
  }

  @override
  void close() {
    // Do a force close to ensure that lingering requests will not stall the
    // program.
    _client.close(force: true);
  }
}

JsonEncoder _jsonEncoder = JsonEncoder.withIndent('  ');

class IOPersistentProperties extends PersistentProperties {
  late final File _file;
  late Map<String, dynamic> _map;

  IOPersistentProperties(String name, {String? documentDirPath}) : super(name) {
    var fileName = '.${name.replaceAll(' ', '_')}';
    documentDirPath ??= userHomeDir();
    _file = File(path.join(documentDirPath, fileName));
    if (!_file.existsSync()) {
      _file.createSync();
    }
    syncSettings();
  }

  IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
    _file = file;
    if (!_file.existsSync()) {
      _file.createSync();
    }
    syncSettings();
  }

  @override
  dynamic operator [](String key) => _map[key];

  @override
  void operator []=(String key, dynamic value) {
    if (value == null && !_map.containsKey(key)) return;
    if (_map[key] == value) return;

    if (value == null) {
      _map.remove(key);
    } else {
      _map[key] = value;
    }

    try {
      _file.writeAsStringSync('${_jsonEncoder.convert(_map)}\n');
    } catch (_) {}
  }

  @override
  void syncSettings() {
    try {
      var contents = _file.readAsStringSync();
      if (contents.isEmpty) contents = '{}';
      _map = jsonDecode(contents) as Map<String, dynamic>;
    } catch (_) {
      _map = {};
    }
  }
}

/// Return the string for the platform's locale; return's `null` if the locale
/// can't be determined.
String? getPlatformLocale() {
  var locale = Platform.localeName;

  // Convert `en_US.UTF-8` to `en_US`.
  var index = locale.indexOf('.');
  if (index != -1) locale = locale.substring(0, index);

  // Convert `en_US` to `en-us`.
  locale = locale.replaceAll('_', '-').toLowerCase();

  return locale;
}
