import { Debouncer } from '@vaadin/component-base/src/debounce.js';
import { timeOut } from '@vaadin/component-base/src/async.js';

window.Vaadin.Flow.virtualListConnector = {
  initLazy: function (list) {
    // Check whether the connector was already initialized for the virtual list
    if (list.$connector) {
      return;
    }

    const extraItemsBuffer = 20;

    let lastRequestedRange = [0, 0];

    list.$connector = {};
    list.$connector.placeholderItem = { __placeholder: true };

    list.itemAccessibleNameGenerator = (item) => item && item.accessibleName;

    const updateRequestedItem = function () {
      /*
       * TODO virtual list seems to do a small index adjustment after scrolling
       * has stopped. This causes a redundant request to be sent to make a
       * corresponding minimal change to the buffer. We should avoid these
       * requests by making the logic skip doing a request if the available
       * buffer is within some tolerance compared to the requested buffer.
       */
      const visibleIndexes = [...list.children]
        .filter((el) => '__virtualListIndex' in el)
        .map((el) => el.__virtualListIndex);
      const firstNeededItem = Math.min(...visibleIndexes);
      const lastNeededItem = Math.max(...visibleIndexes);

      let first = Math.max(0, firstNeededItem - extraItemsBuffer);
      let last = Math.min(lastNeededItem + extraItemsBuffer, list.items.length);

      if (lastRequestedRange[0] != first || lastRequestedRange[1] != last) {
        lastRequestedRange = [first, last];
        const count = 1 + last - first;
        list.$server.setViewportRange(first, count);
      }
    };

    const scheduleUpdateRequest = function () {
      list.__requestDebounce = Debouncer.debounce(list.__requestDebounce, timeOut.after(50), updateRequestedItem);
    };

    requestAnimationFrame(() => updateRequestedItem);

    // Add an observer function that will invoke on virtualList.renderer property
    // change and then patches it with a wrapper renderer
    list.patchVirtualListRenderer = function () {
      if (!list.renderer || list.renderer.__virtualListConnectorPatched) {
        // The list either doesn't have a renderer yet or it's already been patched
        return;
      }

      const originalRenderer = list.renderer;

      const renderer = (root, list, model) => {
        root.__virtualListIndex = model.index;

        if (model.item === undefined) {
          if (list.$connector.placeholderElement) {
            // ComponentRenderer
            if (!root.__hasComponentRendererPlaceholder) {
              // The root was previously rendered by the ComponentRenderer. Clear and add a placeholder.
              root.innerHTML = '';
              delete root._$litPart$;
              root.appendChild(list.$connector.placeholderElement.cloneNode(true));
              root.__hasComponentRendererPlaceholder = true;
            }
          } else {
            // LitRenderer
            originalRenderer.call(list, root, list, {
              ...model,
              item: list.$connector.placeholderItem
            });
          }
        } else {
          if (root.__hasComponentRendererPlaceholder) {
            // The root was previously populated with a placeholder. Clear it.
            root.innerHTML = '';
            root.__hasComponentRendererPlaceholder = false;
          }

          originalRenderer.call(list, root, list, model);
        }

        /*
         * Check if we need to do anything once things have settled down.
         * This method is called multiple times in sequence for the same user
         * action, but we only want to do the check once.
         */
        scheduleUpdateRequest();
      };
      renderer.__virtualListConnectorPatched = true;
      renderer.__rendererId = originalRenderer.__rendererId;

      list.renderer = renderer;
    };

    list._createPropertyObserver('renderer', 'patchVirtualListRenderer', true);
    list.patchVirtualListRenderer();

    list.items = [];

    list.$connector.set = function (index, items) {
      list.items.splice(index, items.length, ...items);
      list.items = [...list.items];
    };

    list.$connector.clear = function (index, length) {
      // How many items, starting from "index", should be set as undefined
      const clearCount = Math.min(length, list.items.length - index);
      list.$connector.set(index, [...Array(clearCount)]);
    };

    list.$connector.updateData = function (items) {
      const updatedItemsMap = items.reduce((map, item) => {
        map[item.key] = item;
        return map;
      }, {});

      list.items = list.items.map((item) => {
        // Items can be undefined if they are outside the viewport
        if (!item) {
          return item;
        }
        // Replace existing item with updated item,
        // return existing item as fallback if it was not updated
        return updatedItemsMap[item.key] || item;
      });
    };

    list.$connector.updateSize = function (newSize) {
      const delta = newSize - list.items.length;
      if (delta > 0) {
        list.items = [...list.items, ...Array(delta)];
      } else if (delta < 0) {
        list.items = list.items.slice(0, newSize);
      }
    };

    list.$connector.setPlaceholderItem = function (placeholderItem = {}, appId) {
      placeholderItem.__placeholder = true;
      list.$connector.placeholderItem = placeholderItem;
      const nodeId = Object.entries(placeholderItem).find(([key]) => key.endsWith('_nodeid'));
      list.$connector.placeholderElement = nodeId ? Vaadin.Flow.clients[appId].getByNodeId(nodeId[1]) : null;
    };
  }
};
