import 'dart:async';

import 'package:dbus/dbus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications_platform_interface/flutter_local_notifications_platform_interface.dart';
import 'package:path/path.dart' as path;

import 'dbus_wrapper.dart';
import 'helpers.dart';
import 'model/capabilities.dart';
import 'model/enums.dart';
import 'model/hint.dart';
import 'model/icon.dart';
import 'model/initialization_settings.dart';
import 'model/location.dart';
import 'model/notification_details.dart';
import 'model/sound.dart';
import 'model/timeout.dart';
import 'notification_info.dart';
import 'platform_info.dart';
import 'storage.dart';

/// Linux notification manager and client
class LinuxNotificationManager {
  /// Constructs an instance of of [LinuxNotificationManager]
  LinuxNotificationManager()
      : _dbus = DBusWrapper(),
        _platformInfo = LinuxPlatformInfo(),
        _storage = NotificationStorage();

  /// Constructs an instance of of [LinuxNotificationManager]
  /// with the given class dependencies.
  @visibleForTesting
  LinuxNotificationManager.private({
    DBusWrapper? dbus,
    LinuxPlatformInfo? platformInfo,
    NotificationStorage? storage,
  })  : _dbus = dbus ?? DBusWrapper(),
        _platformInfo = platformInfo ?? LinuxPlatformInfo(),
        _storage = storage ?? NotificationStorage();

  final DBusWrapper _dbus;
  final LinuxPlatformInfo _platformInfo;
  final NotificationStorage _storage;

  late final LinuxInitializationSettings _initializationSettings;
  late final DidReceiveNotificationResponseCallback?
      _onDidReceiveNotificationResponse;
  late final LinuxPlatformInfoData _platformData;

  bool _initialized = false;

  /// Initializes the manager.
  /// Call this method on application before using the manager further.
  Future<bool> initialize(
    LinuxInitializationSettings initializationSettings, {
    DidReceiveNotificationResponseCallback? onDidReceiveNotificationResponse,
  }) async {
    if (_initialized) {
      return _initialized;
    }
    _initialized = true;
    _initializationSettings = initializationSettings;
    _onDidReceiveNotificationResponse = onDidReceiveNotificationResponse;
    _dbus.build(
      destination: _DBusInterfaceSpec.destination,
      path: _DBusInterfaceSpec.path,
    );
    _platformData = await _platformInfo.getAll();

    await _storage.forceReloadCache();
    _subscribeSignals();
    return _initialized;
  }

  /// Show notification
  Future<void> show(
    int id,
    String? title,
    String? body, {
    LinuxNotificationDetails? details,
    String? payload,
  }) async {
    final LinuxNotificationInfo? prevNotify = await _storage.getById(id);
    final LinuxNotificationIcon? defaultIcon =
        _initializationSettings.defaultIcon;

    final DBusMethodSuccessResponse result = await _dbus.callMethod(
      _DBusInterfaceSpec.destination,
      _DBusMethodsSpec.notify,
      <DBusValue>[
        // app_name
        DBusString(_platformData.appName ?? ''),
        // replaces_id
        DBusUint32(prevNotify?.systemId ?? 0),
        // app_icon
        DBusString(_getAppIcon(details?.icon ?? defaultIcon) ?? ''),
        // summary
        DBusString(title ?? ''),
        // body
        DBusString(body ?? ''),
        // actions
        DBusArray.string(_buildActions(details, _initializationSettings)),
        // hints
        DBusDict.stringVariant(_buildHints(details, _initializationSettings)),
        // expire_timeout
        DBusInt32(
          details?.timeout.value ??
              const LinuxNotificationTimeout.systemDefault().value,
        ),
      ],
      replySignature: DBusSignature('u'),
    );
    final List<LinuxNotificationActionInfo>? actionsInfo = details?.actions
        .map(
          (LinuxNotificationAction action) => LinuxNotificationActionInfo(
            key: action.key,
          ),
        )
        .toList();

    final int systemId = (result.returnValues[0] as DBusUint32).value;
    final LinuxNotificationInfo notify = prevNotify?.copyWith(
          systemId: systemId,
          payload: payload,
          actions: actionsInfo,
        ) ??
        LinuxNotificationInfo(
          id: id,
          systemId: systemId,
          payload: payload,
          actions: actionsInfo ?? <LinuxNotificationActionInfo>[],
        );
    await _storage.insert(notify);
  }

  Map<String, DBusValue> _buildHints(
    LinuxNotificationDetails? details,
    LinuxInitializationSettings initSettings,
  ) {
    final Map<String, DBusValue> hints = <String, DBusValue>{};
    final LinuxNotificationIcon? icon =
        details?.icon ?? initSettings.defaultIcon;
    if (icon?.type == LinuxIconType.byteData) {
      final LinuxRawIconData data = icon!.content as LinuxRawIconData;
      hints['image-data'] = DBusStruct(
        <DBusValue>[
          DBusInt32(data.width),
          DBusInt32(data.height),
          DBusInt32(data.rowStride),
          DBusBoolean(data.hasAlpha),
          DBusInt32(data.bitsPerSample),
          DBusInt32(data.channels),
          DBusArray.byte(data.data),
        ],
      );
    }
    final LinuxNotificationSound? sound =
        details?.sound ?? initSettings.defaultSound;
    if (sound != null) {
      switch (sound.type) {
        case LinuxSoundType.assets:
          hints['sound-file'] = DBusString(
            path.join(
              _platformData.assetsPath!,
              sound.content as String,
            ),
          );
          break;
        case LinuxSoundType.theme:
          hints['sound-name'] = DBusString(sound.content as String);
          break;
      }
    }
    if (details?.category != null) {
      hints['category'] = DBusString(details!.category!.name);
    }
    if (details?.urgency != null) {
      hints['urgency'] = DBusByte(details!.urgency!.index);
    }
    if (details?.resident ?? false) {
      hints['resident'] = const DBusBoolean(true);
    }
    final bool? suppressSound =
        details?.suppressSound ?? initSettings.defaultSuppressSound;
    if (suppressSound ?? false) {
      hints['suppress-sound'] = const DBusBoolean(true);
    }
    if (details?.transient ?? false) {
      hints['transient'] = const DBusBoolean(true);
    }
    if (details?.location != null) {
      final LinuxNotificationLocation location = details!.location!;
      hints['x'] = DBusByte(location.x);
      hints['y'] = DBusByte(location.y);
    }
    if (details?.actionKeyAsIconName ?? false) {
      hints['action-icons'] = const DBusBoolean(true);
    }
    if (details?.customHints != null) {
      hints.addAll(_buildCustomHints(details!.customHints!));
    }

    return hints;
  }

  Map<String, DBusValue> _buildCustomHints(
    List<LinuxNotificationCustomHint> hints,
  ) =>
      Map<String, DBusValue>.fromEntries(
        hints.map(
          (LinuxNotificationCustomHint hint) => MapEntry<String, DBusValue>(
            hint.name,
            hint.value.toDBusValue(),
          ),
        ),
      );

  List<String> _buildActions(
    LinuxNotificationDetails? details,
    LinuxInitializationSettings initSettings,
  ) {
    // Add default action, which is triggered when the notification is clicked
    final List<String> actions = <String>[
      _kDefaultActionName,
      details?.defaultActionName ?? initSettings.defaultActionName,
    ];
    if (details != null) {
      for (final LinuxNotificationAction action in details.actions) {
        actions
          ..add(action.key)
          ..add(action.label);
      }
    }
    return actions;
  }

  /// Cancel notification with the given [id].
  Future<void> cancel(int id) async {
    final LinuxNotificationInfo? notify = await _storage.getById(id);
    await _storage.removeById(id);
    if (notify != null) {
      await _dbusCancel(notify.systemId);
    }
  }

  /// Cancel all notifications.
  Future<void> cancelAll() async {
    final List<LinuxNotificationInfo> notifyList = await _storage.getAll();
    final List<int> idList = <int>[];
    for (final LinuxNotificationInfo notify in notifyList) {
      idList.add(notify.id);
      await _dbusCancel(notify.systemId);
    }
    await _storage.removeByIdList(idList);
  }

  /// Returns the system notification server capabilities.
  Future<LinuxServerCapabilities> getCapabilities() async {
    final DBusMethodSuccessResponse result = await _dbus.callMethod(
      _DBusInterfaceSpec.destination,
      _DBusMethodsSpec.getCapabilities,
      <DBusValue>[],
      replySignature: DBusSignature('as'),
    );
    final Set<String> capsSet = (result.returnValues[0] as DBusArray)
        .children
        .map((DBusValue c) => (c as DBusString).value)
        .toSet();

    final LinuxServerCapabilities capabilities = LinuxServerCapabilities(
      otherCapabilities: const <String>{},
      body: capsSet.remove('body'),
      bodyHyperlinks: capsSet.remove('body-hyperlinks'),
      bodyImages: capsSet.remove('body-images'),
      bodyMarkup: capsSet.remove('body-markup'),
      iconMulti: capsSet.remove('icon-multi'),
      iconStatic: capsSet.remove('icon-static'),
      persistence: capsSet.remove('persistence'),
      sound: capsSet.remove('sound'),
      actions: capsSet.remove('actions'),
      actionIcons: capsSet.remove('action-icons'),
    );
    return capabilities.copyWith(otherCapabilities: capsSet);
  }

  /// Returns a [Map] with the specified notification id as the key
  /// and the id, assigned by the system, as the value.
  Future<Map<int, int>> getSystemIdMap() async =>
      Map<int, int>.fromEntries(await _storage.getAll().then(
            (List<LinuxNotificationInfo> list) => list.map(
              (LinuxNotificationInfo notify) => MapEntry<int, int>(
                notify.id,
                notify.systemId,
              ),
            ),
          ));

  Future<void> _dbusCancel(int systemId) => _dbus.callMethod(
        _DBusInterfaceSpec.destination,
        _DBusMethodsSpec.closeNotification,
        <DBusValue>[DBusUint32(systemId)],
        replySignature: DBusSignature(''),
      );

  String? _getAppIcon(LinuxNotificationIcon? icon) {
    if (icon == null) {
      return null;
    }
    switch (icon.type) {
      case LinuxIconType.assets:
        if (_platformData.assetsPath == null) {
          return null;
        } else {
          final String relativePath = icon.content as String;
          return path.join(_platformData.assetsPath!, relativePath);
        }
      case LinuxIconType.byteData:
        return null;
      case LinuxIconType.theme:
        return icon.content as String;
      case LinuxIconType.filePath:
        return icon.content as String;
    }
  }

  /// Subscribe to the signals for actions and closing notifications.
  void _subscribeSignals() {
    _dbus.subscribeSignal(_DBusMethodsSpec.actionInvoked).listen(
      (DBusSignal s) async {
        if (s.signature != DBusSignature('us')) {
          return;
        }

        final int systemId = (s.values[0] as DBusUint32).value;
        final String actionKey = (s.values[1] as DBusString).value;
        final LinuxNotificationInfo? notify =
            await _storage.getBySystemId(systemId);
        if (notify == null) {
          return;
        }
        if (actionKey == _kDefaultActionName) {
          _onDidReceiveNotificationResponse?.call(
            NotificationResponse(
              id: notify.id,
              payload: notify.payload,
              notificationResponseType:
                  NotificationResponseType.selectedNotification,
            ),
          );
        } else {
          final LinuxNotificationActionInfo? actionInfo =
              notify.actions.firstWhere(
            (LinuxNotificationActionInfo a) => a.key == actionKey,
          );
          if (actionInfo == null) {
            return;
          }
          _onDidReceiveNotificationResponse?.call(
            NotificationResponse(
              id: notify.id,
              actionId: actionInfo.key,
              payload: notify.payload,
              notificationResponseType:
                  NotificationResponseType.selectedNotificationAction,
            ),
          );
        }
      },
    );

    _dbus.subscribeSignal(_DBusMethodsSpec.notificationClosed).listen(
      (DBusSignal s) async {
        if (s.signature != DBusSignature('uu')) {
          return;
        }

        final int systemId = (s.values[0] as DBusUint32).value;
        await _storage.removeBySystemId(systemId);
      },
    );
  }
}

const String _kDefaultActionName = 'default';

class _DBusInterfaceSpec {
  static const String destination = 'org.freedesktop.Notifications';
  static const String path = '/org/freedesktop/Notifications';
}

class _DBusMethodsSpec {
  static const String notify = 'Notify';
  static const String closeNotification = 'CloseNotification';
  static const String actionInvoked = 'ActionInvoked';
  static const String notificationClosed = 'NotificationClosed';
  static const String getCapabilities = 'GetCapabilities';
}
