import 'dart:math';
import 'package:flutter/material.dart';

class AutoCompleteDropdownButton<T> extends StatefulWidget {
  final String label;
  final T? selectedItem;
  final List<T> items;
  final void Function(T? value) onChanged;
  final List<T> Function(String? value)? applyQuery;
  final TextEditingController controller;

  const AutoCompleteDropdownButton(
      {Key? key,
      this.selectedItem,
      required this.label,
      required this.items,
      required this.onChanged,
      this.applyQuery,
      required this.controller})
      : super(key: key);

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

class _AutoCompleteDropdownButtonState<T>
    extends State<AutoCompleteDropdownButton<T>> {
  late List<T> options;
  late List<T> suggestions;

  final FocusNode focusNode = FocusNode();
  final LayerLink layerLink = LayerLink();
  OverlayEntry? entry;
  bool isOpen = false;

  @override
  void initState() {
    super.initState();

    setState(() {
      options = widget.items;
      suggestions = [];
    });
  }

  @override
  void dispose() {
    focusNode.dispose();
    super.dispose();
  }

  void toggleOverlay() {
    isOpen ? hideOverlay() : showOverlay();
  }

  void showOverlay() {
    hideOverlay();
    focusNode.requestFocus();

    List<Widget> items = [];
    Divider? divider;

    ScrollController _scrollController = ScrollController();

    final overlay = Overlay.of(context)!;
    final renderBox = context.findRenderObject() as RenderBox;

    if (widget.selectedItem != null) {
      items.add(buildListTile(widget.selectedItem!));
    }

    if (suggestions.isNotEmpty) {
      items.addAll(suggestions
          .where((item) => item != widget.selectedItem)
          .map((item) => buildListTile(item)));
    }

    if ((widget.selectedItem != null || suggestions.isNotEmpty) &&
        (options.length -
                suggestions.length -
                (widget.selectedItem == null ? 0 : 1) >
            0)) {
      divider = const Divider(height: 10);
      items.add(divider);
    }

    items.addAll(options
        .where((item) =>
            !(widget.selectedItem != null &&
                item.toString() == widget.selectedItem!.toString()) &&
            !suggestions.contains(item))
        .map((item) => buildListTile(item))
        .toList());

    final screenHeight = MediaQuery.of(context).size.height;
    final neededHeight = renderBox.size.height * (items.length - 1) +
        (divider?.height ?? renderBox.size.height);
    final availableHeight = screenHeight -
        (renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height);
    bool displayAbove = neededHeight > availableHeight &&
        screenHeight - availableHeight > availableHeight;
    final height = min(neededHeight,
        max(availableHeight, screenHeight - availableHeight - 55) - 55);

    entry = OverlayEntry(
      builder: (context) => Positioned(
        width: renderBox.size.width,
        height: height,
        child: CompositedTransformFollower(
          link: layerLink,
          targetAnchor: displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
          followerAnchor:
              displayAbove ? Alignment.bottomLeft : Alignment.topLeft,
          offset: Offset(0, renderBox.size.height * (displayAbove ? -1 : 1)),
          showWhenUnlinked: false,
          child: Scrollbar(
            controller: _scrollController,
            isAlwaysShown: true,
            child: Material(
              elevation: 8,
              child: SingleChildScrollView(
                controller: _scrollController,
                child: Column(
                  children: items,
                ),
              ),
            ),
          ),
        ),
      ),
    );

    overlay.insert(entry!);
    isOpen = true;
  }

  ListTile buildListTile(T item) {
    return ListTile(
      onTap: () => handleChanged(item),
      selected: item == widget.selectedItem,
      title: Row(
        children: [
          Expanded(
            child: Text(item.toString()),
          ),
        ],
      ),
    );
  }

  void hideOverlay() {
    entry?.remove();
    setState(() {
      entry = null;
      isOpen = false;
    });
  }

  void handleChanged(T? item) {
    widget.onChanged(item);
    hideOverlay();
  }

  void onChangeQuery(String value) {
    if (value.trim() == '' ||
        (widget.selectedItem != null &&
            value == widget.selectedItem!.toString())) {
      setState(() {
        suggestions = [];
      });
    } else {
      if (widget.applyQuery == null) {
        setState(() {
          suggestions = widget.items.where((item) {
            String itemString = item.toString().toLowerCase();
            String valueLowercase = value.toLowerCase();
            return itemString.contains(valueLowercase);
          }).toList();
        });
      } else {
        setState(() {
          suggestions = widget.applyQuery!(value)
              .where((item) => item != widget.selectedItem)
              .toList();
        });
      }
      showOverlay();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Focus(
      focusNode: focusNode,
      onFocusChange: (isFocused) {
        if (!isFocused) {
          hideOverlay();
        }
      },
      child: CompositedTransformTarget(
        link: layerLink,
        child: TextFormField(
          onChanged: onChangeQuery,
          onTap: toggleOverlay,
          controller: widget.controller,
          decoration: InputDecoration(
            labelText: widget.label,
            suffixIcon: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                widget.selectedItem != null
                    ? IconButton(
                        onPressed: () => handleChanged(null),
                        icon: const Icon(Icons.close),
                        iconSize: 20.0,
                      )
                    : Container(),
                IconButton(
                  onPressed: toggleOverlay,
                  icon: const Icon(Icons.arrow_drop_down),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}
