import 'dart:convert';

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:obtainium/components/custom_app_bar.dart';
import 'package:obtainium/components/generated_form.dart';
import 'package:obtainium/components/generated_form_modal.dart';
import 'package:obtainium/custom_errors.dart';
import 'package:obtainium/main.dart';
import 'package:obtainium/pages/app.dart';
import 'package:obtainium/pages/settings.dart';
import 'package:obtainium/providers/apps_provider.dart';
import 'package:obtainium/providers/settings_provider.dart';
import 'package:obtainium/providers/source_provider.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:markdown/markdown.dart' as md;

class AppsPage extends StatefulWidget {
  const AppsPage({super.key});

  @override
  State<AppsPage> createState() => AppsPageState();
}

showChangeLogDialog(BuildContext context, App app, String? changesUrl,
    AppSource appSource, String changeLog) {
  showDialog(
      context: context,
      builder: (BuildContext context) {
        return GeneratedFormModal(
          title: tr('changes'),
          items: const [],
          message: app.latestVersion,
          additionalWidgets: [
            changesUrl != null
                ? GestureDetector(
                    child: Text(
                      changesUrl,
                      style: const TextStyle(
                          decoration: TextDecoration.underline,
                          fontStyle: FontStyle.italic),
                    ),
                    onTap: () {
                      launchUrlString(changesUrl,
                          mode: LaunchMode.externalApplication);
                    },
                  )
                : const SizedBox.shrink(),
            changesUrl != null
                ? const SizedBox(
                    height: 16,
                  )
                : const SizedBox.shrink(),
            appSource.changeLogIfAnyIsMarkDown
                ? SizedBox(
                    width: MediaQuery.of(context).size.width,
                    height: MediaQuery.of(context).size.height - 350,
                    child: Markdown(
                      data: changeLog,
                      onTapLink: (text, href, title) {
                        if (href != null) {
                          launchUrlString(
                              href.startsWith('http://') ||
                                      href.startsWith('https://')
                                  ? href
                                  : '${Uri.parse(app.url).origin}/$href',
                              mode: LaunchMode.externalApplication);
                        }
                      },
                      extensionSet: md.ExtensionSet(
                        md.ExtensionSet.gitHubFlavored.blockSyntaxes,
                        [
                          md.EmojiSyntax(),
                          ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes
                        ],
                      ),
                    ))
                : Text(changeLog),
          ],
          singleNullReturnButton: tr('ok'),
        );
      });
}

getChangeLogFn(BuildContext context, App app) {
  AppSource appSource =
      SourceProvider().getSource(app.url, overrideSource: app.overrideSource);
  String? changesUrl = appSource.changeLogPageFromStandardUrl(app.url);
  String? changeLog = app.changeLog;
  if (changeLog?.split('\n').length == 1) {
    if (RegExp(
            '(http|ftp|https)://([\\w_-]+(?:(?:\\.[\\w_-]+)+))([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])?')
        .hasMatch(changeLog!)) {
      if (changesUrl == null) {
        changesUrl = changeLog;
        changeLog = null;
      }
    }
  }
  return (changeLog == null && changesUrl == null)
      ? null
      : () {
          if (changeLog != null) {
            showChangeLogDialog(context, app, changesUrl, appSource, changeLog);
          } else {
            launchUrlString(changesUrl!, mode: LaunchMode.externalApplication);
          }
        };
}

class AppsPageState extends State<AppsPage> {
  AppsFilter filter = AppsFilter();
  final AppsFilter neutralFilter = AppsFilter();
  var updatesOnlyFilter =
      AppsFilter(includeUptodate: false, includeNonInstalled: false);
  Set<String> selectedAppIds = {};
  DateTime? refreshingSince;

  clearSelected() {
    if (selectedAppIds.isNotEmpty) {
      setState(() {
        selectedAppIds.clear();
      });
      return true;
    }
    return false;
  }

  selectThese(List<App> apps) {
    if (selectedAppIds.isEmpty) {
      setState(() {
        for (var a in apps) {
          selectedAppIds.add(a.id);
        }
      });
    }
  }

  final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey =
      GlobalKey<RefreshIndicatorState>();

  @override
  Widget build(BuildContext context) {
    var appsProvider = context.watch<AppsProvider>();
    var settingsProvider = context.watch<SettingsProvider>();
    var sourceProvider = SourceProvider();
    var listedApps = appsProvider.getAppValues().toList();

    refresh() {
      HapticFeedback.lightImpact();
      setState(() {
        refreshingSince = DateTime.now();
      });
      return appsProvider.checkUpdates().catchError((e) {
        showError(e is Map ? e['errors'] : e, context);
        return <App>[];
      }).whenComplete(() {
        setState(() {
          refreshingSince = null;
        });
      });
    }

    if (!appsProvider.loadingApps &&
        appsProvider.apps.isNotEmpty &&
        settingsProvider.checkJustStarted() &&
        settingsProvider.checkOnStart) {
      _refreshIndicatorKey.currentState?.show();
    }

    selectedAppIds = selectedAppIds
        .where((element) => listedApps.map((e) => e.app.id).contains(element))
        .toSet();

    toggleAppSelected(App app) {
      setState(() {
        if (selectedAppIds.map((e) => e).contains(app.id)) {
          selectedAppIds.removeWhere((a) => a == app.id);
        } else {
          selectedAppIds.add(app.id);
        }
      });
    }

    listedApps = listedApps.where((app) {
      if (app.app.installedVersion == app.app.latestVersion &&
          !(filter.includeUptodate)) {
        return false;
      }
      if (app.app.installedVersion == null && !(filter.includeNonInstalled)) {
        return false;
      }
      if (filter.nameFilter.isNotEmpty || filter.authorFilter.isNotEmpty) {
        List<String> nameTokens = filter.nameFilter
            .split(' ')
            .where((element) => element.trim().isNotEmpty)
            .toList();
        List<String> authorTokens = filter.authorFilter
            .split(' ')
            .where((element) => element.trim().isNotEmpty)
            .toList();

        for (var t in nameTokens) {
          if (!app.name.toLowerCase().contains(t.toLowerCase())) {
            return false;
          }
        }
        for (var t in authorTokens) {
          if (!app.app.author.toLowerCase().contains(t.toLowerCase())) {
            return false;
          }
        }
      }
      if (filter.idFilter.isNotEmpty) {
        if (!app.app.id.contains(filter.idFilter)) {
          return false;
        }
      }
      if (filter.categoryFilter.isNotEmpty &&
          filter.categoryFilter
              .intersection(app.app.categories.toSet())
              .isEmpty) {
        return false;
      }
      if (filter.sourceFilter.isNotEmpty &&
          sourceProvider
                  .getSource(app.app.url,
                      overrideSource: app.app.overrideSource)
                  .runtimeType
                  .toString() !=
              filter.sourceFilter) {
        return false;
      }
      return true;
    }).toList();

    listedApps.sort((a, b) {
      int result = 0;
      if (settingsProvider.sortColumn == SortColumnSettings.authorName) {
        result = ((a.app.author + a.name).toLowerCase())
            .compareTo((b.app.author + b.name).toLowerCase());
      } else if (settingsProvider.sortColumn == SortColumnSettings.nameAuthor) {
        result = ((a.name + a.app.author).toLowerCase())
            .compareTo((b.name + b.app.author).toLowerCase());
      } else if (settingsProvider.sortColumn ==
          SortColumnSettings.releaseDate) {
        result = (a.app.releaseDate)?.compareTo(
                b.app.releaseDate ?? DateTime.fromMicrosecondsSinceEpoch(0)) ??
            0;
      }
      return result;
    });

    if (settingsProvider.sortOrder == SortOrderSettings.descending) {
      listedApps = listedApps.reversed.toList();
    }

    var existingUpdates = appsProvider.findExistingUpdates(installedOnly: true);

    var existingUpdateIdsAllOrSelected = existingUpdates
        .where((element) => selectedAppIds.isEmpty
            ? listedApps.where((a) => a.app.id == element).isNotEmpty
            : selectedAppIds.map((e) => e).contains(element))
        .toList();
    var newInstallIdsAllOrSelected = appsProvider
        .findExistingUpdates(nonInstalledOnly: true)
        .where((element) => selectedAppIds.isEmpty
            ? listedApps.where((a) => a.app.id == element).isNotEmpty
            : selectedAppIds.map((e) => e).contains(element))
        .toList();

    List<String> trackOnlyUpdateIdsAllOrSelected = [];
    existingUpdateIdsAllOrSelected = existingUpdateIdsAllOrSelected.where((id) {
      if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
        trackOnlyUpdateIdsAllOrSelected.add(id);
        return false;
      }
      return true;
    }).toList();
    newInstallIdsAllOrSelected = newInstallIdsAllOrSelected.where((id) {
      if (appsProvider.apps[id]!.app.additionalSettings['trackOnly'] == true) {
        trackOnlyUpdateIdsAllOrSelected.add(id);
        return false;
      }
      return true;
    }).toList();

    if (settingsProvider.pinUpdates) {
      var temp = [];
      listedApps = listedApps.where((sa) {
        if (existingUpdates.contains(sa.app.id)) {
          temp.add(sa);
          return false;
        }
        return true;
      }).toList();
      listedApps = [...temp, ...listedApps];
    }

    if (settingsProvider.buryNonInstalled) {
      var temp = [];
      listedApps = listedApps.where((sa) {
        if (sa.app.installedVersion == null) {
          temp.add(sa);
          return false;
        }
        return true;
      }).toList();
      listedApps = [...listedApps, ...temp];
    }

    var tempPinned = [];
    var tempNotPinned = [];
    for (var a in listedApps) {
      if (a.app.pinned) {
        tempPinned.add(a);
      } else {
        tempNotPinned.add(a);
      }
    }
    listedApps = [...tempPinned, ...tempNotPinned];

    List<String?> getListedCategories() {
      var temp = listedApps
          .map((e) => e.app.categories.isNotEmpty ? e.app.categories : [null]);
      return temp.isNotEmpty
          ? {
              ...temp.reduce((v, e) => [...v, ...e])
            }.toList()
          : [];
    }

    var listedCategories = getListedCategories();
    listedCategories.sort((a, b) {
      return a != null && b != null
          ? a.toLowerCase().compareTo(b.toLowerCase())
          : a == null
              ? 1
              : -1;
    });

    Set<App> selectedApps = listedApps
        .map((e) => e.app)
        .where((a) => selectedAppIds.contains(a.id))
        .toSet();

    getLoadingWidgets() {
      return [
        if (listedApps.isEmpty)
          SliverFillRemaining(
              child: Center(
                  child: Text(
            appsProvider.apps.isEmpty ? tr('noApps') : tr('noAppsForFilter'),
            style: Theme.of(context).textTheme.headlineMedium,
            textAlign: TextAlign.center,
          ))),
        if (refreshingSince != null || appsProvider.loadingApps)
          SliverToBoxAdapter(
            child: LinearProgressIndicator(
              value: appsProvider.loadingApps
                  ? null
                  : appsProvider
                          .getAppValues()
                          .where((element) => !(element.app.lastUpdateCheck
                                  ?.isBefore(refreshingSince!) ??
                              true))
                          .length /
                      (appsProvider.apps.isNotEmpty
                          ? appsProvider.apps.length
                          : 1),
            ),
          )
      ];
    }

    getUpdateButton(int appIndex) {
      return IconButton(
          visualDensity: VisualDensity.compact,
          color: Theme.of(context).colorScheme.primary,
          tooltip:
              listedApps[appIndex].app.additionalSettings['trackOnly'] == true
                  ? tr('markUpdated')
                  : tr('update'),
          onPressed: appsProvider.areDownloadsRunning()
              ? null
              : () {
                  appsProvider.downloadAndInstallLatestApps(
                      [listedApps[appIndex].app.id],
                      globalNavigatorKey.currentContext).catchError((e) {
                    showError(e, context);
                    return <String>[];
                  });
                },
          icon: Icon(
              listedApps[appIndex].app.additionalSettings['trackOnly'] == true
                  ? Icons.check_circle_outline
                  : Icons.install_mobile));
    }

    getAppIcon(int appIndex) {
      return listedApps[appIndex].icon != null
          ? Image.memory(
              listedApps[appIndex].icon!,
              gaplessPlayback: true,
            )
          : Row(
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                  Transform(
                      alignment: Alignment.center,
                      transform: Matrix4.rotationZ(0.31),
                      child: Padding(
                        padding: const EdgeInsets.all(15),
                        child: Image(
                          image: const AssetImage(
                              'assets/graphics/icon_small.png'),
                          color: Colors.white.withOpacity(0.3),
                          colorBlendMode: BlendMode.modulate,
                          gaplessPlayback: true,
                        ),
                      )),
                ]);
    }

    getVersionText(int appIndex) {
      return '${listedApps[appIndex].app.installedVersion ?? tr('notInstalled')}${listedApps[appIndex].app.additionalSettings['trackOnly'] == true ? ' ${tr('pseudoVersion')}' : ''}';
    }

    getChangesButtonString(int appIndex, bool hasChangeLogFn) {
      return listedApps[appIndex].app.releaseDate == null
          ? hasChangeLogFn
              ? tr('changes')
              : ''
          : DateFormat('yyyy-MM-dd')
              .format(listedApps[appIndex].app.releaseDate!);
    }

    getSingleAppHorizTile(int index) {
      var showChangesFn = getChangeLogFn(context, listedApps[index].app);
      var hasUpdate = listedApps[index].app.installedVersion != null &&
          listedApps[index].app.installedVersion !=
              listedApps[index].app.latestVersion;
      Widget trailingRow = Row(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          hasUpdate ? getUpdateButton(index) : const SizedBox.shrink(),
          hasUpdate
              ? const SizedBox(
                  width: 10,
                )
              : const SizedBox.shrink(),
          GestureDetector(
              onTap: showChangesFn,
              child: Container(
                  decoration: BoxDecoration(
                      borderRadius: BorderRadius.circular(12),
                      color: settingsProvider.highlightTouchTargets &&
                              showChangesFn != null
                          ? (Theme.of(context).brightness == Brightness.light
                                  ? Theme.of(context).primaryColor
                                  : Theme.of(context).primaryColorLight)
                              .withAlpha(20)
                          : null),
                  padding: settingsProvider.highlightTouchTargets
                      ? const EdgeInsetsDirectional.fromSTEB(12, 0, 12, 0)
                      : const EdgeInsetsDirectional.fromSTEB(24, 0, 0, 0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    crossAxisAlignment: CrossAxisAlignment.end,
                    children: [
                      Row(mainAxisSize: MainAxisSize.min, children: [
                        Container(
                            constraints: BoxConstraints(
                                maxWidth:
                                    MediaQuery.of(context).size.width / 4),
                            child: Text(getVersionText(index),
                                overflow: TextOverflow.ellipsis,
                                textAlign: TextAlign.end)),
                      ]),
                      Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Text(
                            getChangesButtonString(
                                index, showChangesFn != null),
                            style: TextStyle(
                                fontStyle: FontStyle.italic,
                                decoration: showChangesFn != null
                                    ? TextDecoration.underline
                                    : TextDecoration.none),
                          )
                        ],
                      ),
                    ],
                  )))
        ],
      );

      var transparent =
          Theme.of(context).colorScheme.background.withAlpha(0).value;
      List<double> stops = [
        ...listedApps[index].app.categories.asMap().entries.map(
            (e) => ((e.key / (listedApps[index].app.categories.length - 1)))),
        1
      ];
      if (stops.length == 2) {
        stops[0] = 1;
      }
      return Container(
          decoration: BoxDecoration(
              gradient: LinearGradient(
                  stops: stops,
                  begin: const Alignment(-1, 0),
                  end: const Alignment(-0.97, 0),
                  colors: [
                ...listedApps[index].app.categories.map((e) =>
                    Color(settingsProvider.categories[e] ?? transparent)
                        .withAlpha(255)),
                Color(transparent)
              ])),
          child: ListTile(
            tileColor: listedApps[index].app.pinned
                ? Colors.grey.withOpacity(0.1)
                : Colors.transparent,
            selectedTileColor: Theme.of(context)
                .colorScheme
                .primary
                .withOpacity(listedApps[index].app.pinned ? 0.2 : 0.1),
            selected:
                selectedAppIds.map((e) => e).contains(listedApps[index].app.id),
            onLongPress: () {
              toggleAppSelected(listedApps[index].app);
            },
            leading: getAppIcon(index),
            title: Text(
              maxLines: 1,
              listedApps[index].name,
              style: TextStyle(
                overflow: TextOverflow.ellipsis,
                fontWeight: listedApps[index].app.pinned
                    ? FontWeight.bold
                    : FontWeight.normal,
              ),
            ),
            subtitle: Text(tr('byX', args: [listedApps[index].app.author]),
                maxLines: 1,
                style: TextStyle(
                    overflow: TextOverflow.ellipsis,
                    fontWeight: listedApps[index].app.pinned
                        ? FontWeight.bold
                        : FontWeight.normal)),
            trailing: listedApps[index].downloadProgress != null
                ? SizedBox(
                    child: Text(
                    listedApps[index].downloadProgress! >= 0
                        ? tr('percentProgress', args: [
                            listedApps[index]
                                .downloadProgress!
                                .toInt()
                                .toString()
                          ])
                        : tr('installing'),
                    textAlign: (listedApps[index].downloadProgress! >= 0)
                        ? TextAlign.start
                        : TextAlign.end,
                  ))
                : trailingRow,
            onTap: () {
              if (selectedAppIds.isNotEmpty) {
                toggleAppSelected(listedApps[index].app);
              } else {
                Navigator.push(
                  context,
                  MaterialPageRoute(
                      builder: (context) =>
                          AppPage(appId: listedApps[index].app.id)),
                );
              }
            },
          ));
    }

    getCategoryCollapsibleTile(int index) {
      var tiles = listedApps
          .asMap()
          .entries
          .where((e) =>
              e.value.app.categories.contains(listedCategories[index]) ||
              e.value.app.categories.isEmpty && listedCategories[index] == null)
          .map((e) => getSingleAppHorizTile(e.key))
          .toList();

      capFirstChar(String str) => str[0].toUpperCase() + str.substring(1);
      return ExpansionTile(
          initiallyExpanded: true,
          title: Text(
            capFirstChar(listedCategories[index] ?? tr('noCategory')),
            style: const TextStyle(fontWeight: FontWeight.bold),
          ),
          controlAffinity: ListTileControlAffinity.leading,
          trailing: Text(tiles.length.toString()),
          children: tiles);
    }

    getSelectAllButton() {
      return selectedAppIds.isEmpty
          ? TextButton.icon(
              style: const ButtonStyle(visualDensity: VisualDensity.compact),
              onPressed: () {
                selectThese(listedApps.map((e) => e.app).toList());
              },
              icon: Icon(
                Icons.select_all_outlined,
                color: Theme.of(context).colorScheme.primary,
              ),
              label: Text(listedApps.length.toString()))
          : TextButton.icon(
              style: const ButtonStyle(visualDensity: VisualDensity.compact),
              onPressed: () {
                selectedAppIds.isEmpty
                    ? selectThese(listedApps.map((e) => e.app).toList())
                    : clearSelected();
              },
              icon: Icon(
                selectedAppIds.isEmpty
                    ? Icons.select_all_outlined
                    : Icons.deselect_outlined,
                color: Theme.of(context).colorScheme.primary,
              ),
              label: Text(selectedAppIds.length.toString()));
    }

    getMassObtainFunction() {
      return appsProvider.areDownloadsRunning() ||
              (existingUpdateIdsAllOrSelected.isEmpty &&
                  newInstallIdsAllOrSelected.isEmpty &&
                  trackOnlyUpdateIdsAllOrSelected.isEmpty)
          ? null
          : () {
              HapticFeedback.heavyImpact();
              List<GeneratedFormItem> formItems = [];
              if (existingUpdateIdsAllOrSelected.isNotEmpty) {
                formItems.add(GeneratedFormSwitch('updates',
                    label: tr('updateX', args: [
                      plural('apps', existingUpdateIdsAllOrSelected.length)
                    ]),
                    defaultValue: true));
              }
              if (newInstallIdsAllOrSelected.isNotEmpty) {
                formItems.add(GeneratedFormSwitch('installs',
                    label: tr('installX', args: [
                      plural('apps', newInstallIdsAllOrSelected.length)
                    ]),
                    defaultValue: existingUpdateIdsAllOrSelected.isEmpty));
              }
              if (trackOnlyUpdateIdsAllOrSelected.isNotEmpty) {
                formItems.add(GeneratedFormSwitch('trackonlies',
                    label: tr('markXTrackOnlyAsUpdated', args: [
                      plural('apps', trackOnlyUpdateIdsAllOrSelected.length)
                    ]),
                    defaultValue: existingUpdateIdsAllOrSelected.isEmpty &&
                        newInstallIdsAllOrSelected.isEmpty));
              }
              showDialog<Map<String, dynamic>?>(
                  context: context,
                  builder: (BuildContext ctx) {
                    var totalApps = existingUpdateIdsAllOrSelected.length +
                        newInstallIdsAllOrSelected.length +
                        trackOnlyUpdateIdsAllOrSelected.length;
                    return GeneratedFormModal(
                      title: tr('changeX', args: [plural('apps', totalApps)]),
                      items: formItems.map((e) => [e]).toList(),
                      initValid: true,
                    );
                  }).then((values) async {
                if (values != null) {
                  if (values.isEmpty) {
                    values = getDefaultValuesFromFormItems([formItems]);
                  }
                  bool shouldInstallUpdates = values['updates'] == true;
                  bool shouldInstallNew = values['installs'] == true;
                  bool shouldMarkTrackOnlies = values['trackonlies'] == true;
                  List<String> toInstall = [];
                  if (shouldInstallUpdates) {
                    toInstall.addAll(existingUpdateIdsAllOrSelected);
                  }
                  if (shouldInstallNew) {
                    toInstall.addAll(newInstallIdsAllOrSelected);
                  }
                  if (shouldMarkTrackOnlies) {
                    toInstall.addAll(trackOnlyUpdateIdsAllOrSelected);
                  }
                  appsProvider
                      .downloadAndInstallLatestApps(
                          toInstall, globalNavigatorKey.currentContext)
                      .catchError((e) {
                    showError(e, context);
                    return <String>[];
                  }).then((value) {
                    if (value.isNotEmpty && shouldInstallUpdates) {
                      showMessage(tr('appsUpdated'), context);
                    }
                  });
                }
              });
            };
    }

    launchCategorizeDialog() {
      return () async {
        try {
          Set<String>? preselected;
          var showPrompt = false;
          for (var element in selectedApps) {
            var currentCats = element.categories.toSet();
            if (preselected == null) {
              preselected = currentCats;
            } else {
              if (!settingsProvider.setEqual(currentCats, preselected)) {
                showPrompt = true;
                break;
              }
            }
          }
          var cont = true;
          if (showPrompt) {
            cont = await showDialog<Map<String, dynamic>?>(
                    context: context,
                    builder: (BuildContext ctx) {
                      return GeneratedFormModal(
                        title: tr('categorize'),
                        items: const [],
                        initValid: true,
                        message: tr('selectedCategorizeWarning'),
                      );
                    }) !=
                null;
          }
          if (cont) {
            // ignore: use_build_context_synchronously
            await showDialog<Map<String, dynamic>?>(
                context: context,
                builder: (BuildContext ctx) {
                  return GeneratedFormModal(
                    title: tr('categorize'),
                    items: const [],
                    initValid: true,
                    singleNullReturnButton: tr('continue'),
                    additionalWidgets: [
                      CategoryEditorSelector(
                        preselected: !showPrompt ? preselected ?? {} : {},
                        showLabelWhenNotEmpty: false,
                        onSelected: (categories) {
                          appsProvider.saveApps(selectedApps.map((e) {
                            e.categories = categories;
                            return e;
                          }).toList());
                        },
                      )
                    ],
                  );
                });
          }
        } catch (err) {
          showError(err, context);
        }
      };
    }

    showMassMarkDialog() {
      return showDialog(
          context: context,
          builder: (BuildContext ctx) {
            return AlertDialog(
              title: Text(tr('markXSelectedAppsAsUpdated',
                  args: [selectedAppIds.length.toString()])),
              content: Text(
                tr('onlyWorksWithNonVersionDetectApps'),
                style: const TextStyle(
                    fontWeight: FontWeight.bold, fontStyle: FontStyle.italic),
              ),
              actions: [
                TextButton(
                    onPressed: () {
                      Navigator.of(context).pop();
                    },
                    child: Text(tr('no'))),
                TextButton(
                    onPressed: () {
                      HapticFeedback.selectionClick();
                      appsProvider.saveApps(selectedApps.map((a) {
                        if (a.installedVersion != null &&
                            !appsProvider.isVersionDetectionPossible(
                                appsProvider.apps[a.id])) {
                          a.installedVersion = a.latestVersion;
                        }
                        return a;
                      }).toList());

                      Navigator.of(context).pop();
                    },
                    child: Text(tr('yes')))
              ],
            );
          }).whenComplete(() {
        Navigator.of(context).pop();
      });
    }

    pinSelectedApps() {
      var pinStatus = selectedApps.where((element) => element.pinned).isEmpty;
      appsProvider.saveApps(selectedApps.map((e) {
        e.pinned = pinStatus;
        return e;
      }).toList());
      Navigator.of(context).pop();
    }

    resetSelectedAppsInstallStatuses() async {
      try {
        var values = await showDialog(
            context: context,
            builder: (BuildContext ctx) {
              return GeneratedFormModal(
                title: tr('resetInstallStatusForSelectedAppsQuestion'),
                items: const [],
                initValid: true,
                message: tr('installStatusOfXWillBeResetExplanation',
                    args: [plural('apps', selectedAppIds.length)]),
              );
            });
        if (values != null) {
          appsProvider.saveApps(selectedApps.map((e) {
            e.installedVersion = null;
            return e;
          }).toList());
        }
      } finally {
        Navigator.of(context).pop();
      }
    }

    showMoreOptionsDialog() {
      return showDialog(
          context: context,
          builder: (BuildContext ctx) {
            return AlertDialog(
              scrollable: true,
              content: Padding(
                padding: const EdgeInsets.only(top: 6),
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.spaceAround,
                    children: [
                      TextButton(
                          onPressed: pinSelectedApps,
                          child: Text(selectedApps
                                  .where((element) => element.pinned)
                                  .isEmpty
                              ? tr('pinToTop')
                              : tr('unpinFromTop'))),
                      const Divider(),
                      TextButton(
                          onPressed: () {
                            String urls = '';
                            for (var a in selectedApps) {
                              urls += '${a.url}\n';
                            }
                            urls = urls.substring(0, urls.length - 1);
                            Share.share(urls,
                                subject: 'Obtainium - ${tr('appsString')}');
                            Navigator.of(context).pop();
                          },
                          child: Text(tr('shareSelectedAppURLs'))),
                      const Divider(),
                      TextButton(
                          onPressed: selectedAppIds.isEmpty
                              ? null
                              : () {
                                  String urls =
                                      '<p>${tr('customLinkMessage')}:</p>\n\n<ul>\n';
                                  for (var a in selectedApps) {
                                    urls +=
                                        '    <li><a href="obtainium://app/${Uri.encodeComponent(jsonEncode({
                                          'id': a.id,
                                          'url': a.url,
                                          'author': a.author,
                                          'name': a.name,
                                          'preferredApkIndex':
                                              a.preferredApkIndex,
                                          'additionalSettings':
                                              jsonEncode(a.additionalSettings)
                                        }))}">${a.name}</a></li>\n';
                                  }
                                  urls +=
                                      '</ul>\n\n<p><a href="$obtainiumUrl">${tr('about')}</a></p>';
                                  Share.share(urls,
                                      subject:
                                          'Obtainium - ${tr('appsString')}');
                                },
                          child: Text(tr('shareAppConfigLinks'))),
                      const Divider(),
                      TextButton(
                          onPressed: () {
                            appsProvider
                                .downloadAppAssets(
                                    selectedApps.map((e) => e.id).toList(),
                                    globalNavigatorKey.currentContext ??
                                        context)
                                .catchError((e) => showError(
                                    e,
                                    globalNavigatorKey.currentContext ??
                                        context));
                            Navigator.of(context).pop();
                          },
                          child: Text(tr('downloadX',
                              args: [tr('releaseAsset').toLowerCase()]))),
                      const Divider(),
                      TextButton(
                          onPressed: appsProvider.areDownloadsRunning()
                              ? null
                              : showMassMarkDialog,
                          child: Text(tr('markSelectedAppsUpdated'))),
                    ]),
              ),
            );
          });
    }

    getMainBottomButtons() {
      return [
        IconButton(
            visualDensity: VisualDensity.compact,
            onPressed: getMassObtainFunction(),
            tooltip: selectedAppIds.isEmpty
                ? tr('installUpdateApps')
                : tr('installUpdateSelectedApps'),
            icon: const Icon(
              Icons.file_download_outlined,
            )),
        IconButton(
          visualDensity: VisualDensity.compact,
          onPressed: selectedAppIds.isEmpty
              ? null
              : () {
                  appsProvider.removeAppsWithModal(
                      context, selectedApps.toList());
                },
          tooltip: tr('removeSelectedApps'),
          icon: const Icon(Icons.delete_outline_outlined),
        ),
        IconButton(
          visualDensity: VisualDensity.compact,
          onPressed: selectedAppIds.isEmpty ? null : launchCategorizeDialog(),
          tooltip: tr('categorize'),
          icon: const Icon(Icons.category_outlined),
        ),
        IconButton(
          visualDensity: VisualDensity.compact,
          onPressed: selectedAppIds.isEmpty ? null : showMoreOptionsDialog,
          tooltip: tr('more'),
          icon: const Icon(Icons.more_horiz),
        ),
      ];
    }

    showFilterDialog() async {
      var values = await showDialog<Map<String, dynamic>?>(
          context: context,
          builder: (BuildContext ctx) {
            var vals = filter.toFormValuesMap();
            return GeneratedFormModal(
              initValid: true,
              title: tr('filterApps'),
              items: [
                [
                  GeneratedFormTextField('appName',
                      label: tr('appName'),
                      required: false,
                      defaultValue: vals['appName']),
                  GeneratedFormTextField('author',
                      label: tr('author'),
                      required: false,
                      defaultValue: vals['author'])
                ],
                [
                  GeneratedFormTextField('appId',
                      label: tr('appId'),
                      required: false,
                      defaultValue: vals['appId'])
                ],
                [
                  GeneratedFormSwitch('upToDateApps',
                      label: tr('upToDateApps'),
                      defaultValue: vals['upToDateApps'])
                ],
                [
                  GeneratedFormSwitch('nonInstalledApps',
                      label: tr('nonInstalledApps'),
                      defaultValue: vals['nonInstalledApps'])
                ],
                [
                  GeneratedFormDropdown(
                      'sourceFilter',
                      label: tr('appSource'),
                      defaultValue: filter.sourceFilter,
                      [
                        MapEntry('', tr('none')),
                        ...sourceProvider.sources.map(
                            (e) => MapEntry(e.runtimeType.toString(), e.name))
                      ])
                ]
              ],
              additionalWidgets: [
                const SizedBox(
                  height: 16,
                ),
                CategoryEditorSelector(
                  preselected: filter.categoryFilter,
                  onSelected: (categories) {
                    filter.categoryFilter = categories.toSet();
                  },
                )
              ],
            );
          });
      if (values != null) {
        setState(() {
          filter.setFormValuesFromMap(values);
        });
      }
    }

    getFilterButtonsRow() {
      var isFilterOff = filter.isIdenticalTo(neutralFilter, settingsProvider);
      return Row(
        children: [
          getSelectAllButton(),
          IconButton(
              color: Theme.of(context).colorScheme.primary,
              style: const ButtonStyle(visualDensity: VisualDensity.compact),
              tooltip: '${tr('filter')}${isFilterOff ? '' : ' *'}',
              onPressed: isFilterOff
                  ? showFilterDialog
                  : () {
                      setState(() {
                        filter = AppsFilter();
                      });
                    },
              icon: Icon(isFilterOff
                  ? Icons.filter_list_rounded
                  : Icons.filter_list_off_rounded)),
          const SizedBox(
            width: 10,
          ),
          const VerticalDivider(),
          Expanded(
              child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: getMainBottomButtons(),
          )),
        ],
      );
    }

    getDisplayedList() {
      return settingsProvider.groupByCategory &&
              !(listedCategories.isEmpty ||
                  (listedCategories.length == 1 && listedCategories[0] == null))
          ? SliverList(
              delegate:
                  SliverChildBuilderDelegate((BuildContext context, int index) {
              return getCategoryCollapsibleTile(index);
            }, childCount: listedCategories.length))
          : SliverList(
              delegate:
                  SliverChildBuilderDelegate((BuildContext context, int index) {
              return getSingleAppHorizTile(index);
            }, childCount: listedApps.length));
    }

    return Scaffold(
      backgroundColor: Theme.of(context).colorScheme.surface,
      body: RefreshIndicator(
          key: _refreshIndicatorKey,
          onRefresh: refresh,
          child: CustomScrollView(slivers: <Widget>[
            CustomAppBar(title: tr('appsString')),
            ...getLoadingWidgets(),
            getDisplayedList()
          ])),
      persistentFooterButtons: appsProvider.apps.isEmpty
          ? null
          : [
              getFilterButtonsRow(),
            ],
    );
  }
}

class AppsFilter {
  late String nameFilter;
  late String authorFilter;
  late String idFilter;
  late bool includeUptodate;
  late bool includeNonInstalled;
  late Set<String> categoryFilter;
  late String sourceFilter;

  AppsFilter(
      {this.nameFilter = '',
      this.authorFilter = '',
      this.idFilter = '',
      this.includeUptodate = true,
      this.includeNonInstalled = true,
      this.categoryFilter = const {},
      this.sourceFilter = ''});

  Map<String, dynamic> toFormValuesMap() {
    return {
      'appName': nameFilter,
      'author': authorFilter,
      'appId': idFilter,
      'upToDateApps': includeUptodate,
      'nonInstalledApps': includeNonInstalled,
      'sourceFilter': sourceFilter
    };
  }

  setFormValuesFromMap(Map<String, dynamic> values) {
    nameFilter = values['appName']!;
    authorFilter = values['author']!;
    idFilter = values['appId']!;
    includeUptodate = values['upToDateApps'];
    includeNonInstalled = values['nonInstalledApps'];
    sourceFilter = values['sourceFilter'];
  }

  bool isIdenticalTo(AppsFilter other, SettingsProvider settingsProvider) =>
      authorFilter.trim() == other.authorFilter.trim() &&
      nameFilter.trim() == other.nameFilter.trim() &&
      idFilter.trim() == other.idFilter.trim() &&
      includeUptodate == other.includeUptodate &&
      includeNonInstalled == other.includeNonInstalled &&
      settingsProvider.setEqual(categoryFilter, other.categoryFilter) &&
      sourceFilter.trim() == other.sourceFilter.trim();
}
