import 'dart:async';

import 'package:flutter/material.dart';

import 'dropdown_search.dart';

class SelectDialog<T> extends StatefulWidget {
  final T? selectedValue;
  final List<T>? items;
  final bool showSearchBox;
  final bool isFilteredOnline;
  final ValueChanged<T>? onChanged;
  final DropdownSearchOnFind<T>? onFind;
  final DropdownSearchPopupItemBuilder<T>? itemBuilder;
  final InputDecoration? searchBoxDecoration;
  final DropdownSearchItemAsString<T>? itemAsString;
  final DropdownSearchFilterFn<T>? filterFn;
  final String? hintText;
  final double? maxHeight;
  final double? dialogMaxWidth;
  final Widget? popupTitle;
  final bool showSelectedItem;
  final DropdownSearchCompareFn<T>? compareFn;
  final DropdownSearchPopupItemEnabled<T>? itemDisabled;

  ///custom layout for empty results
  final EmptyBuilder? emptyBuilder;

  ///custom layout for loading items
  final LoadingBuilder? loadingBuilder;

  ///custom layout for error
  final ErrorBuilder? errorBuilder;

  ///the search box will be focused if true
  final bool autoFocusSearchBox;

  ///text controller to set default search word for example
  final TextEditingController? searchBoxController;

  ///delay before searching
  final Duration? searchDelay;

  ///show or hide favorites items
  final bool showFavoriteItems;

  ///build favorites chips
  final FavoriteItemsBuilder<T>? favoriteItemBuilder;

  ///favorite items alignment
  final MainAxisAlignment? favoriteItemsAlignment;

  ///favorites item
  final FavoriteItems<T>? favoriteItems;

  const SelectDialog({
    Key? key,
    this.popupTitle,
    this.items,
    this.maxHeight,
    this.showSearchBox = false,
    this.isFilteredOnline = false,
    this.onChanged,
    this.selectedValue,
    this.onFind,
    this.itemBuilder,
    this.searchBoxDecoration,
    this.hintText,
    this.itemAsString,
    this.filterFn,
    this.showSelectedItem = false,
    this.compareFn,
    this.emptyBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.autoFocusSearchBox = false,
    this.dialogMaxWidth,
    this.itemDisabled,
    this.searchBoxController,
    this.searchDelay,
    this.favoriteItemBuilder,
    this.favoriteItems,
    this.showFavoriteItems = false,
    this.favoriteItemsAlignment = MainAxisAlignment.start,
  }) : super(key: key);

  @override
  _SelectDialogState<T> createState() => _SelectDialogState<T>();
}

class _SelectDialogState<T> extends State<SelectDialog<T?>> {
  final FocusNode focusNode = new FocusNode();
  final StreamController<List<T?>> _itemsStream = StreamController<List<T?>>.broadcast();
  final ValueNotifier<bool> _loadingNotifier = ValueNotifier(false);
  final List<T?> _items = <T>[];
  late Debouncer _debouncer;

  @override
  void initState() {
    super.initState();
    _debouncer = Debouncer(delay: widget.searchDelay);

    Future.delayed(
      Duration.zero,
      () => manageItemsByFilter(widget.searchBoxController?.text ?? '', isFistLoad: true),
    );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (widget.autoFocusSearchBox) FocusScope.of(context).requestFocus(focusNode);
  }

  @override
  void dispose() {
    _itemsStream.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Size deviceSize = MediaQuery.of(context).size;
    bool isTablet = deviceSize.width > deviceSize.height;
    double maxHeight = deviceSize.height * (isTablet ? .8 : .6);
    double maxWidth = deviceSize.width * (isTablet ? .7 : .9);

    return Container(
      width: widget.dialogMaxWidth ?? maxWidth,
      constraints: BoxConstraints(maxHeight: widget.maxHeight ?? maxHeight),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          _searchField(),
          if (widget.showFavoriteItems == true) _favoriteItemsWidget(),
          Expanded(
            child: Stack(
              children: <Widget>[
                StreamBuilder<List<T?>>(
                  stream: _itemsStream.stream,
                  builder: (context, snapshot) {
                    if (snapshot.hasError) {
                      return _errorWidget(snapshot.error);
                    } else if (!snapshot.hasData) {
                      return _loadingWidget();
                    } else if (snapshot.data!.isEmpty) {
                      if (widget.emptyBuilder != null)
                        return widget.emptyBuilder!(context, widget.searchBoxController?.text);
                      else
                        return const Center(
                          child: const Text("No data found"),
                        );
                    }
                    return ListView.builder(
                      shrinkWrap: true,
                      padding: EdgeInsets.symmetric(vertical: 0),
                      itemCount: snapshot.data!.length,
                      itemBuilder: (context, index) {
                        var item = snapshot.data![index];
                        return _itemWidget(item);
                      },
                    );
                  },
                ),
                _loadingWidget()
              ],
            ),
          ),
        ],
      ),
    );
  }

  void _showErrorDialog(dynamic error) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text("Error while getting online items"),
          content: _errorWidget(error),
          actions: <Widget>[
            TextButton(
              child: new Text("OK"),
              onPressed: () {
                Navigator.of(context).pop(false);
              },
            )
          ],
        );
      },
    );
  }

  Widget _errorWidget(dynamic error) {
    if (widget.errorBuilder != null)
      return widget.errorBuilder!(context, widget.searchBoxController?.text, error);
    else
      return Padding(
        padding: EdgeInsets.all(8),
        child: Text(
          error?.toString() ?? 'Error',
        ),
      );
  }

  Widget _loadingWidget() {
    return ValueListenableBuilder(
        valueListenable: _loadingNotifier,
        builder: (context, bool isLoading, wid) {
          if (isLoading) {
            if (widget.loadingBuilder != null)
              return widget.loadingBuilder!(context, widget.searchBoxController?.text);
            else
              return Padding(
                padding: const EdgeInsets.all(24.0),
                child: const Center(
                  child: const CircularProgressIndicator(),
                ),
              );
          }
          return Container();
        });
  }

  void _onTextChanged(String filter) async {
    manageItemsByFilter(filter);
  }

  ///Function that filter item (online and offline) base on user filter
  ///[filter] is the filter keyword
  ///[isFirstLoad] true if it's the first time we load data from online, false other wises
  void manageItemsByFilter(String filter, {bool isFistLoad = false}) async {
    _loadingNotifier.value = true;

    String encoded(String item) {
      String encodedItem = "";
      for (int i = 0; i < item.length; i++) {
        var char = item[i];
        switch (char) {
          case 'Á':
          case 'á':
          case 'ą':
          case 'ä':
            char = 'a';
            break;
          case 'é':
          case 'É':
            char = 'e';
            break;
          case 'ú':
          case 'ű':
          case 'ü':
          case 'Ú':
          case 'Ű':
          case 'Ü':
            char = 'u';
            break;
          case 'ö':
          case 'ő':
          case 'ó':
          case 'Ö':
          case 'Ő':
          case 'Ó':
            char = 'o';
            break;
          case 'í':
          case 'Í':
            char = 'i';
            break;
        }
        encodedItem += char;
      }
      return encodedItem;
    }

    List<T?> applyFilter(String filter) {
      return _items.where((i) {
        if (widget.filterFn != null)
          return (widget.filterFn!(i, filter));
        else if (i.toString().toLowerCase().contains(filter.toLowerCase()) ||
            encoded(i.toString()).toLowerCase().contains(encoded(filter.toLowerCase()))) {
          return true;
        } else if (widget.itemAsString != null) {
          bool found = (widget.itemAsString!(i)).toLowerCase().contains(filter.toLowerCase());
          if (!found) {
            found = (encoded(widget.itemAsString!(i))).toLowerCase().contains(encoded(filter.toLowerCase()));
          }
          return found;
        }
        return false;
      }).toList();
    }

    //load offline data for the first time
    if (isFistLoad && widget.items != null) _items.addAll(widget.items!);

    //manage offline items
    if (widget.onFind != null && (widget.isFilteredOnline || isFistLoad)) {
      try {
        final List<T?> onlineItems = [];
        onlineItems.addAll(await widget.onFind!(filter));

        //Remove all old data
        _items.clear();
        //add offline items
        if (widget.items != null) {
          _items.addAll(widget.items!);
          //if filter online we filter only local list based on entered keyword (filter)
          if (widget.isFilteredOnline == true) {
            var filteredLocalList = applyFilter(filter);
            _items.clear();
            _items.addAll(filteredLocalList);
          }
        }
        //add new online items to list
        _items.addAll(onlineItems);

        //don't filter data , they are already filtred online and local data are already filtered
        if (widget.isFilteredOnline == true)
          _addDataToStream(_items);
        else
          _addDataToStream(applyFilter(filter));
      } catch (e) {
        _addErrorToStream(e);
        //if offline items count > 0 , the error will be not visible for the user
        //As solution we show it in dialog
        if (widget.items != null && widget.items!.isNotEmpty) {
          _showErrorDialog(e);
          _addDataToStream(applyFilter(filter));
        }
      }
    } else {
      _addDataToStream(applyFilter(filter));
    }

    _loadingNotifier.value = false;
  }

  void _addDataToStream(List<T?> data) {
    if (_itemsStream.isClosed) return;
    _itemsStream.add(data);
  }

  void _addErrorToStream(Object error, [StackTrace? stackTrace]) {
    if (_itemsStream.isClosed) return;
    _itemsStream.addError(error, stackTrace);
  }

  Widget _itemWidget(T? item) {
    if (widget.itemBuilder != null)
      return InkWell(
        child: widget.itemBuilder!(
          context,
          item,
          _manageSelectedItemVisibility(item),
        ),
        onTap: widget.itemDisabled != null && (widget.itemDisabled!(item)) == true ? null : () => _handleSelectItem(item),
      );
    else
      return ListTile(
        title: Text(_selectedItemAsString(item)),
        selected: _manageSelectedItemVisibility(item),
        onTap: widget.itemDisabled != null && (widget.itemDisabled!(item)) == true ? null : () => _handleSelectItem(item),
      );
  }

  /// selected item will be highlighted only when [widget.showSelectedItem] is true,
  /// if our object is String [widget.compareFn] is not required , other wises it's required
  bool _manageSelectedItemVisibility(T? item) {
    if (!widget.showSelectedItem) return false;

    if (item is String?) {
      return item == widget.selectedValue;
    } else {
      return widget.compareFn!(item, widget.selectedValue);
    }
  }

  Widget _searchField() {
    return Column(crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: <Widget>[
      widget.popupTitle ?? const SizedBox.shrink(),
      if (widget.showSearchBox)
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: TextField(
            controller: widget.searchBoxController,
            focusNode: focusNode,
            onChanged: (f) => _debouncer(() {
              _onTextChanged(f);
            }),
            decoration: widget.searchBoxDecoration ??
                InputDecoration(
                  hintText: widget.hintText,
                  border: const OutlineInputBorder(),
                  contentPadding: const EdgeInsets.symmetric(horizontal: 16),
                ),
          ),
        )
    ]);
  }

  Widget _favoriteItemsWidget() {
    return StreamBuilder<List<T?>>(
        stream: _itemsStream.stream,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return _buildFavoriteItems(widget.favoriteItems!(snapshot.data!));
          } else {
            return Container();
          }
        });
  }

  Widget _buildFavoriteItems(List<T?>? favoriteItems) {
    if (favoriteItems != null) {
      return Container(
        padding: EdgeInsets.symmetric(horizontal: 8),
        child: LayoutBuilder(builder: (context, constraints) {
          return SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: ConstrainedBox(
              constraints: BoxConstraints(minWidth: constraints.maxWidth),
              child: Row(
                  mainAxisSize: MainAxisSize.max,
                  mainAxisAlignment: widget.favoriteItemsAlignment ?? MainAxisAlignment.start,
                  children: favoriteItems
                      .map(
                        (f) => GestureDetector(
                          onTap: () => _handleSelectItem(f),
                          child: Container(
                            margin: EdgeInsets.only(right: 4),
                            child: widget.favoriteItemBuilder != null
                                ? widget.favoriteItemBuilder!(context, f)
                                : _favoriteItemDefaultWidget(f),
                          ),
                        ),
                      )
                      .toList()),
            ),
          );
        }),
      );
    } else {
      return Container();
    }
  }

  void _handleSelectItem(T? selectedItem) {
    Navigator.pop(context, selectedItem);
    if (widget.onChanged != null) widget.onChanged!(selectedItem);
  }

  Widget _favoriteItemDefaultWidget(T? item) {
    return Container(
      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 6),
      decoration: BoxDecoration(borderRadius: BorderRadius.circular(10), color: Theme.of(context).primaryColorLight),
      child: Text(
        _selectedItemAsString(item),
        textAlign: TextAlign.center,
        style: Theme.of(context).textTheme.subtitle1,
      ),
    );
  }

  ///function that return the String value of an object
  String _selectedItemAsString(T? data) {
    if (data == null) {
      return "";
    } else if (widget.itemAsString != null) {
      return widget.itemAsString!(data);
    } else {
      return data.toString();
    }
  }
}

class Debouncer {
  final Duration? delay;
  Timer? _timer;

  Debouncer({this.delay});

  call(Function action) {
    _timer?.cancel();
    _timer = Timer(delay ?? const Duration(milliseconds: 500), action as void Function());
  }
}