<?php

namespace Components\Form;

class PillBox extends BaseField
{
    /**
     * @param mixed $raw
     * @return array{id:string,name:string,readonly:bool}|null
     */
    protected static function normalizeToken($raw): ?array
    {
        if (!is_array($raw)) {
            if ($raw === null || $raw === '') {
                return null;
            }
            $id = (string)$raw;
            return ['id' => $id, 'name' => $id, 'readonly' => false];
        }

        $id = $raw['id'] ?? ($raw['value'] ?? ($raw['key'] ?? null));
        if ($id === null || $id === '') {
            return null;
        }
        $id = (string)$id;

        $name = $raw['name'] ?? ($raw['label'] ?? ($raw['text'] ?? ''));
        $name = trim((string)$name);
        if ($name === '') {
            $name = $id;
        }

        return [
            'id' => $id,
            'name' => $name,
            'readonly' => !empty($raw['readonly']),
        ];
    }

    /**
     * @param mixed $items
     * @return array<int, array{id:string,name:string,readonly:bool}>
     */
    protected static function normalizeDataset($items): array
    {
        if (!is_array($items)) {
            return [];
        }

        $out = [];
        $seen = [];
        $isAssoc = array_keys($items) !== range(0, count($items) - 1);

        if ($isAssoc) {
            foreach ($items as $key => $item) {
                if (is_array($item)) {
                    $item['id'] = $item['id'] ?? (string)$key;
                    $token = self::normalizeToken($item);
                } else {
                    $token = self::normalizeToken([
                        'id' => (string)$key,
                        'name' => (string)$item,
                    ]);
                }
                if ($token === null) {
                    continue;
                }
                if (isset($seen[$token['id']])) {
                    continue;
                }
                $seen[$token['id']] = true;
                $out[] = $token;
            }
            return $out;
        }

        foreach ($items as $item) {
            $token = self::normalizeToken($item);
            if ($token === null) {
                continue;
            }
            if (isset($seen[$token['id']])) {
                continue;
            }
            $seen[$token['id']] = true;
            $out[] = $token;
        }

        return $out;
    }

    /**
     * @param mixed $value
     * @param array<string, array{id:string,name:string,readonly:bool}> $datasetById
     * @return array<int, array{id:string,name:string,readonly:bool}>
     */
    protected static function normalizeSelected($value, array $datasetById = []): array
    {
        if (is_string($value)) {
            $trimmed = trim($value);
            if ($trimmed === '') {
                $value = [];
            } else {
                $decoded = json_decode($trimmed, true);
                if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
                    $value = $decoded;
                } else {
                    $value = array_map('trim', explode(',', $trimmed));
                }
            }
        }

        $items = is_array($value) ? $value : [];
        $restrictToDataset = !empty($datasetById);
        $out = [];
        $seen = [];
        foreach ($items as $item) {
            if (!is_array($item) && isset($datasetById[(string)$item])) {
                $token = $datasetById[(string)$item];
            } else {
                $token = self::normalizeToken($item);
            }
            if ($token === null) {
                continue;
            }
            if ($restrictToDataset && !isset($datasetById[$token['id']])) {
                continue;
            }
            if (isset($seen[$token['id']])) {
                continue;
            }
            if (isset($datasetById[$token['id']])) {
                $token['name'] = $datasetById[$token['id']]['name'];
                if (!empty($datasetById[$token['id']]['readonly'])) {
                    $token['readonly'] = true;
                }
            }
            $seen[$token['id']] = true;
            $out[] = $token;
        }
        return $out;
    }

    /**
     * @param mixed $value
     */
    public static function render(string $name, string $label, $value = [], array $opts = []): string
    {
        $errors = $opts['errors'] ?? [];
        $help = $opts['help'] ?? '';
        $badge = $opts['label_badge_html'] ?? '';

        $searchUrl = (string)($opts['search_url'] ?? '');
        $mode = strtolower((string)($opts['mode'] ?? ($searchUrl !== '' ? 'remote' : 'local')));
        if ($mode !== 'remote') {
            $mode = 'local';
        }
        $multiple = !isset($opts['multiple']) || (bool)$opts['multiple'];
        $isReadonly = !empty($opts['is_readonly']);

        $items = self::normalizeDataset($opts['items'] ?? ($opts['local_items'] ?? []));
        $itemsById = [];
        foreach ($items as $item) {
            $itemsById[$item['id']] = $item;
        }

        $initial = self::normalizeSelected($opts['initial_selected'] ?? $value, $itemsById);
        if (!$multiple && count($initial) > 1) {
            $initial = [reset($initial)];
        }

        $placeholder = (string)($opts['search_placeholder'] ?? ($opts['placeholder'] ??
            \mh_lng('UI_PILLBOX_PLACEHOLDER', \mh_lng('UI_SEARCH', 'Search...'))));
        $hintText = (string)($opts['hint_text'] ?? \mh_lng('UI_PILLBOX_HINT_MIN_CHARS', 'Type more characters to search'));
        $emptyText = (string)($opts['no_results_text'] ?? ($opts['no_results_found_text'] ?? ($opts['empty_text'] ?? \mh_lng('UI_NO_RESULTS_FOUND', 'No results found'))));
        $loadingText = (string)($opts['loading_text'] ?? \mh_lng('UI_LOADING', 'Loading...'));
        $errorText = (string)($opts['error_text'] ?? \mh_lng('UI_REQUEST_FAILED', 'Request failed'));

        $minChars = isset($opts['min_chars']) ? max(0, (int)$opts['min_chars']) : 2;
        $delay = isset($opts['delay']) ? max(0, (int)$opts['delay']) : 250;
        $maxItems = isset($opts['max_items']) && (int)$opts['max_items'] > 0 ? (int)$opts['max_items'] : null;
        $datasetKey = (string)($opts['dataset_key'] ?? $name);

        $hasArraySuffix = (strlen($name) >= 2 && substr($name, -2) === '[]');
        $hiddenName = $multiple
            ? ($hasArraySuffix ? $name : ($name . '[]'))
            : ($hasArraySuffix ? substr($name, 0, -2) : $name);
        $modelField = $hasArraySuffix ? substr($name, 0, -2) : $name;

        $config = [
            'mode' => $mode,
            'multiple' => $multiple,
            'readonly' => $isReadonly,
            'localItems' => $items,
            'initialSelected' => $initial,
            'searchUrl' => $searchUrl,
            'datasetKey' => $datasetKey,
            'minChars' => $minChars,
            'delay' => $delay,
            'maxItems' => $maxItems,
            'placeholder' => $placeholder,
            'hintText' => $hintText,
            'emptyText' => $emptyText,
            'loadingText' => $loadingText,
            'errorText' => $errorText,
            'modelField' => $modelField,
        ];

        $configJson = json_encode($config, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
        if (!is_string($configJson) || $configJson === '') {
            $configJson = '{}';
        }

        $uid = substr(md5($name . '|' . uniqid((string)mt_rand(), true)), 0, 12);
        $listboxId = self::h('pillbox-listbox-' . $uid);
        $configEsc = self::h($configJson, false);
        $nameEsc = self::h($name);
        $hiddenNameEsc = self::h((string)$hiddenName);
        $placeholderEsc = self::h($placeholder);

        $helpHtml = self::helpHtml($help);
        $errorHtml = self::errorsHtml($errors);
        $clientErr = self::clientErrorHtml($name);
        $hasError = !empty($errors);

        $inputAreaClass = self::groupClass($hasError, $isReadonly) . ' relative w-full min-h-9 py-1 pl-1.5! pr-2! ';
        if (!$isReadonly) {
            $inputAreaClass .= ' cursor-text';
        }
        $inputAreaClassEsc = self::h($inputAreaClass);

        $rightIconSvg = $mode === 'remote' ? \Components\Base::SVG_SEARCH : \Components\Base::SVG_CHEVRON_DOWN;
        $rightIconClass = $mode === 'remote' ? '[&>svg]:size-4!' : '';

        $labelHtml = '';
        if (!($opts['no_label'] ?? false)) {
            $labelHtml = self::renderLabel('', $label, $badge, $opts);
        }

        $singleHidden = '';
        if (!$multiple) {
            $singleHidden = <<<HTML
<input type="hidden" name="{$hiddenNameEsc}" :value="selected.length ? selected[0].id : ''" />
HTML;
        }

        $inputInner = <<<HTML
<div x-data="mbPillBoxComponent({$configEsc})" x-init="init()" @click.outside="close()" class="relative w-full">
  <div class="{$inputAreaClassEsc} group" @click="focusInput()">
    <div class="flex flex-wrap items-center gap-1 pr-12">
      <template x-for="item in selected" :key="'pill-' + item.id">
        <span class="inline-flex overflow-hidden transition-[max-width,opacity,margin] duration-[320ms] ease-[cubic-bezier(0.2,0,0,1)]" :class="isRemoving(item.id) ? 'max-w-0 opacity-0 me-0' : 'opacity-100'" :style="pillStyle(item)">
          <span class="inline-flex max-w-full items-center gap-1 rounded-xs bg-gray-100 pl-2 pr-1 py-0.5 text-sm text-gray-800 transition-[filter,opacity,transform] duration-[280ms] ease-in dark:bg-gray-700 dark:text-gray-100" :class="isRemoving(item.id) ? 'scale-95 blur-sm opacity-0' : (isMarked(item.id) ? 'scale-100 blur-0 opacity-100 bg-brand! text-brand-50!' : 'scale-100 blur-0 opacity-100')">
            <span class="truncate" x-text="item.name"></span>
            <button type="button" class="rounded p-0.5 opacity-50 hover:opacity-90 " x-show="!readonly && !item.readonly && !isRemoving(item.id)" @click.stop="removeItem(item.id)">
              <svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z"></path></svg>
            </button>
          </span>
        </span>
      </template>

      <input
        x-ref="input"
        type="text"
        role="combobox"
        aria-autocomplete="list"
        :aria-expanded="open ? 'true' : 'false'"
        aria-controls="{$listboxId}"
        placeholder="{$placeholderEsc}"
        class="min-w-[3ch] flex-1 border-0 bg-transparent py-0 pr-0 text-base text-gray-900 outline-none placeholder:text-gray-400 sm:text-sm/6 dark:text-white dark:placeholder:text-gray-500"
        :class="selected.length > 0 ? 'pl-0' : 'pl-3'"
        x-model="query"
        :readonly="readonly ? true : null"
        :placeholder="selected.length === 0 ? placeholder : ''"
        :style="inputStyle()"
        @focus="openDropdown(); clearMarked()"
        @input="onInput()"
        @keydown.down.prevent="moveActive(1)"
        @keydown.up.prevent="moveActive(-1)"
        @keydown.left="onArrowLeft(\$event)"
        @keydown.right="onArrowRight(\$event)"
        @keydown.enter.prevent="selectByEnter()"
        @keydown.esc.prevent="close()"
        @keydown.tab="close()"
        @keydown.backspace="onBackspace()"
        @blur="close()"
      />
    </div>
    <span class="pointer-events-none absolute top-2.5 right-3 z-10 flex items-start text-secondary/30 group-hover:text-primary {$rightIconClass} " aria-hidden="true">
      {$rightIconSvg}
    </span>
  </div>

  <div x-show="open" x-cloak x-transition.origin.top id="{$listboxId}" role="listbox" aria-multiselectable="true" class="absolute z-20 mt-1 max-h-60 w-full origin-top overflow-auto rounded-2xl bg-white py-1 shadow-lg outline-1 outline-black/5 dark:bg-gray-800 dark:outline-white/10">
    <div class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400" x-show="status === 'hint'" x-text="hintText"></div>
    <div class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400" x-show="status === 'loading'" x-text="loadingText"></div>
    <div class="px-3 py-2 text-sm text-red-600 dark:text-red-300" x-show="status === 'error'" x-text="errorText"></div>
    <div class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400" x-show="status === 'empty'" x-text="emptyText"></div>

    <template x-if="status === 'results'">
      <ul>
        <template x-for="(item, idx) in filteredResults()" :key="'opt-' + item.id">
          <li role="option"
              class="relative cursor-pointer py-2 pr-4 pl-4 text-sm text-gray-700 select-none dark:text-white"
              :class="idx === activeIndex ? 'bg-gray-100 text-gray-900 dark:bg-brand-600 dark:text-white' : 'hover:bg-gray-100 dark:hover:bg-white/10'"
              @mouseenter="activeIndex = idx"
              @mousedown.prevent="selectIndex(idx)">
            <span x-text="item.name"></span>
          </li>
        </template>
      </ul>
    </template>
  </div>

  <template x-if="multiple">
    <template x-for="item in selected" :key="'hidden-' + item.id">
      <input type="hidden" name="{$hiddenNameEsc}" :value="item.id" :disabled="isRemoving(item.id)" />
    </template>
  </template>
  {$singleHidden}
  <input type="hidden" name="{$nameEsc}" value="" x-ref="modelSync" x-show="false" x-bind:disabled="true" />
</div>
HTML;

        $script = <<<'HTML'
<script>
(function () {
  if (window.mbPillBoxComponent) {
    return;
  }

  window.mbPillBoxComponent = function (cfg) {
    return {
      cfg: cfg || {},
      selected: [],
      query: '',
      open: false,
      loading: false,
      status: 'hint',
      errorText: '',
      activeIndex: -1,
      results: [],
      localItems: [],
      knownById: {},
      timer: null,
      abortController: null,
      remoteCache: {},
      removingIds: {},
      removeTimers: {},
      markedId: null,
      multiple: true,
      readonly: false,
      minChars: 2,
      delay: 250,
      maxItems: null,
      mode: 'local',
      placeholder: '',
      hintText: '',
      emptyText: '',
      loadingText: '',
      datasetKey: '',
      modelField: '',

      init: function () {
        this.mode = (this.cfg.mode || 'local') === 'remote' ? 'remote' : 'local';
        this.multiple = this.cfg.multiple !== false;
        this.readonly = !!this.cfg.readonly;
        this.minChars = Number.isFinite(Number(this.cfg.minChars)) ? Math.max(0, Number(this.cfg.minChars)) : 2;
        this.delay = Number.isFinite(Number(this.cfg.delay)) ? Math.max(0, Number(this.cfg.delay)) : 250;
        this.maxItems = Number.isFinite(Number(this.cfg.maxItems)) && Number(this.cfg.maxItems) > 0 ? Number(this.cfg.maxItems) : null;
        this.placeholder = this.cfg.placeholder || '';
        this.hintText = this.cfg.hintText || '';
        this.emptyText = this.cfg.emptyText || 'No results found';
        this.loadingText = this.cfg.loadingText || 'Loading...';
        this.errorText = this.cfg.errorText || 'Request failed';
        this.datasetKey = this.cfg.datasetKey || '';
        this.modelField = this.cfg.modelField || this.datasetKey || '';

        if (!window.__mbPillBoxCache) {
          window.__mbPillBoxCache = {};
        }
        if (this.datasetKey !== '') {
          if (!window.__mbPillBoxCache[this.datasetKey]) {
            window.__mbPillBoxCache[this.datasetKey] = { remote: {} };
          }
          this.remoteCache = window.__mbPillBoxCache[this.datasetKey].remote;
        }

        var local = Array.isArray(this.cfg.localItems) ? this.cfg.localItems : [];
        for (var i = 0; i < local.length; i++) {
          var localItem = this.normalizeItem(local[i]);
          if (!localItem) {
            continue;
          }
          this.remember(localItem);
          this.localItems.push(localItem);
        }

        var initial = Array.isArray(this.cfg.initialSelected) ? this.cfg.initialSelected : [];
        for (var j = 0; j < initial.length; j++) {
          var selectedItem = this.normalizeItem(initial[j]);
          if (!selectedItem) {
            continue;
          }
          this.remember(selectedItem);
          this.addItem(selectedItem, true);
        }

        if (this.mode === 'local') {
          this.results = this.localItems.slice();
        }

        this.updateStatus();
        this.emitFieldSync();
      },

      selectedIds: function () {
        var out = [];
        for (var i = 0; i < this.selected.length; i++) {
          var item = this.selected[i];
          if (!item || this.isRemoving(item.id)) {
            continue;
          }
          out.push(String(item.id));
        }
        return out;
      },

      emitFieldSync: function () {
        var name = this.modelField || '';
        if (!name) {
          return;
        }

        var ids = this.selectedIds();
        var value = this.multiple ? ids.slice() : (ids.length ? ids[0] : '');

        if (typeof this.$dispatch === 'function') {
          this.$dispatch('mb-field-sync', { name: name, value: value });
        }

        if (this.$refs && this.$refs.modelSync) {
          this.$refs.modelSync.value = this.multiple ? ids.join(',') : String(value || '');
          this.$refs.modelSync.dispatchEvent(new Event('input', { bubbles: true }));
          this.$refs.modelSync.dispatchEvent(new Event('change', { bubbles: true }));
        }
      },

      normalizeItem: function (raw) {
        if (!raw || typeof raw !== 'object') {
          if (raw === null || raw === undefined || raw === '') {
            return null;
          }
          var scalarId = String(raw);
          return { id: scalarId, name: scalarId, readonly: false };
        }
        var id = raw.id;
        if (id === undefined || id === null || id === '') {
          id = raw.value;
        }
        if (id === undefined || id === null || id === '') {
          id = raw.key;
        }
        if (id === undefined || id === null || id === '') {
          return null;
        }

        var name = raw.name;
        if (name === undefined || name === null || String(name).trim() === '') {
          name = raw.label;
        }
        if (name === undefined || name === null || String(name).trim() === '') {
          name = raw.text;
        }
        if (name === undefined || name === null || String(name).trim() === '') {
          name = id;
        }

        return {
          id: String(id),
          name: String(name),
          readonly: !!raw.readonly,
        };
      },

      remember: function (item) {
        if (!item || !item.id) {
          return;
        }
        if (!this.knownById[item.id]) {
          this.knownById[item.id] = {
            id: item.id,
            name: item.name,
            readonly: !!item.readonly,
          };
          return;
        }
        if (this.knownById[item.id].name === item.id && item.name !== item.id) {
          this.knownById[item.id].name = item.name;
        }
        if (item.readonly) {
          this.knownById[item.id].readonly = true;
        }
      },

      focusInput: function () {
        if (this.$refs && this.$refs.input) {
          this.$refs.input.focus();
        }
      },

      isRemoving: function (id) {
        return !!this.removingIds[String(id)];
      },

      isMarked: function (id) {
        if (this.markedId === null || this.markedId === undefined) {
          return false;
        }
        return String(this.markedId) === String(id);
      },

      clearMarked: function () {
        this.markedId = null;
      },

      getMarkedIndex: function () {
        if (this.markedId === null || this.markedId === undefined) {
          return -1;
        }
        var sid = String(this.markedId);
        for (var i = 0; i < this.selected.length; i++) {
          if (this.selected[i].id === sid && !this.isRemoving(sid)) {
            return i;
          }
        }
        return -1;
      },

      markIndex: function (idx) {
        if (idx < 0 || idx >= this.selected.length) {
          this.markedId = null;
          return;
        }
        if (this.isRemoving(this.selected[idx].id)) {
          this.markedId = null;
          return;
        }
        this.markedId = this.selected[idx].id;
      },

      getLastEditableIndex: function () {
        for (var i = this.selected.length - 1; i >= 0; i--) {
          if (!this.selected[i].readonly && !this.isRemoving(this.selected[i].id)) {
            return i;
          }
        }
        return -1;
      },

      pillStyle: function (item) {
        var text = '';
        if (item && item.name !== undefined && item.name !== null) {
          text = String(item.name);
        }
        var maxCh = Math.min(64, Math.max(8, text.length + 6));
        if (this.isRemoving(item && item.id !== undefined ? item.id : '')) {
          return 'max-width: 0; margin-right: 0; transition-delay: 60ms;';
        }
        return 'max-width: ' + maxCh + 'ch;';
      },

      inputStyle: function () {
        var query = this.query || '';
        if (this.selected.length === 0) {
          return 'width: 100%;';
        }
        var ch = Math.max(3, query.length + 1);
        return 'width: ' + ch + 'ch;';
      },

      openDropdown: function () {
        this.open = true;
        this.updateStatus();
      },

      close: function () {
        this.open = false;
        this.activeIndex = -1;
        this.clearMarked();
        this.clearUnselectedQuery();
      },

      clearUnselectedQuery: function () {
        if ((this.query || '') === '') {
          return;
        }
        this.query = '';
        if (this.mode === 'local') {
          this.results = this.localItems.slice();
        } else {
          this.results = [];
        }
      },

      isSelected: function (id) {
        var sid = String(id);
        for (var i = 0; i < this.selected.length; i++) {
          if (this.selected[i].id === sid) {
            return true;
          }
        }
        return false;
      },

      filteredResults: function () {
        var out = [];
        for (var i = 0; i < this.results.length; i++) {
          var item = this.results[i];
          if (!this.isSelected(item.id)) {
            out.push(item);
          }
        }
        return out;
      },

      updateStatus: function () {
        if (this.loading) {
          this.status = 'loading';
          return;
        }
        if (!this.open) {
          this.status = 'hint';
          return;
        }
        if (this.mode === 'remote' && (this.query || '').length < this.minChars) {
          this.status = 'hint';
          return;
        }
        if (this.mode === 'local' && this.minChars > 0 && (this.query || '').length < this.minChars) {
          this.status = 'hint';
          return;
        }

        var list = this.filteredResults();
        this.status = list.length > 0 ? 'results' : 'empty';
        if (this.status === 'results') {
          if (this.activeIndex < 0 || this.activeIndex >= list.length) {
            this.activeIndex = 0;
          }
        } else {
          this.activeIndex = -1;
        }
      },

      onInput: function () {
        if (this.readonly) {
          return;
        }
        this.clearMarked();
        this.open = true;
        if (this.timer) {
          clearTimeout(this.timer);
        }
        var self = this;

        if (this.mode === 'remote') {
          if ((this.query || '').length < this.minChars) {
            this.results = [];
            this.loading = false;
            this.updateStatus();
            return;
          }
          this.timer = setTimeout(function () {
            self.searchRemote();
          }, this.delay);
          return;
        }

        if (this.minChars > 0 && (this.query || '').length < this.minChars) {
          this.results = [];
          this.updateStatus();
          return;
        }
        this.timer = setTimeout(function () {
          self.searchLocal();
        }, this.delay);
      },

      searchLocal: function () {
        var q = (this.query || '').toLowerCase();
        var out = [];
        for (var i = 0; i < this.localItems.length; i++) {
          var item = this.localItems[i];
          if (q === '' || item.name.toLowerCase().indexOf(q) !== -1) {
            out.push(item);
          }
        }
        this.results = out;
        this.updateStatus();
      },

      searchRemote: function () {
        var query = this.query || '';
        var cacheKey = query.toLowerCase();
        if (this.remoteCache && this.remoteCache[cacheKey]) {
          this.results = this.remoteCache[cacheKey].slice();
          this.loading = false;
          this.updateStatus();
          return;
        }

        var baseUrl = this.cfg.searchUrl || '';
        if (!baseUrl) {
          this.results = [];
          this.loading = false;
          this.updateStatus();
          return;
        }

        if (this.abortController) {
          this.abortController.abort();
        }
        this.abortController = (window.AbortController ? new AbortController() : null);
        this.loading = true;
        this.updateStatus();

        var url = baseUrl;
        try {
          var u = new URL(baseUrl, window.location.origin);
          u.searchParams.set('q', query);
          url = u.toString();
        } catch (e) {
          url = baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'q=' + encodeURIComponent(query);
        }

        var self = this;
        var options = {
          headers: {
            'Accept': 'application/json',
            'HX-Request': 'true'
          }
        };
        if (this.abortController) {
          options.signal = this.abortController.signal;
        }

        fetch(url, options)
          .then(function (response) {
            if (!response.ok) {
              throw new Error('HTTP_' + response.status);
            }
            return response.json();
          })
          .then(function (payload) {
            var rows = Array.isArray(payload) ? payload : (Array.isArray(payload.items) ? payload.items : []);
            var out = [];
            for (var i = 0; i < rows.length; i++) {
              var token = self.normalizeItem(rows[i]);
              if (!token) {
                continue;
              }
              self.remember(token);
              out.push(self.knownById[token.id]);
            }
            self.results = out;
            if (self.remoteCache) {
              self.remoteCache[cacheKey] = out.slice();
            }
            self.loading = false;
            self.status = 'results';
            self.updateStatus();
          })
          .catch(function (error) {
            if (error && error.name === 'AbortError') {
              return;
            }
            self.loading = false;
            self.results = [];
            self.status = 'error';
            self.activeIndex = -1;
          });
      },

      addItem: function (rawItem, force) {
        var normalized = this.normalizeItem(rawItem);
        if (!normalized) {
          return false;
        }
        var known = this.knownById[normalized.id];
        if (!known && !force) {
          return false;
        }
        if (!known) {
          known = normalized;
          this.remember(known);
        }
        if (this.isSelected(known.id)) {
          return false;
        }
        if (this.maxItems !== null && this.multiple && this.selected.length >= this.maxItems) {
          return false;
        }
        if (!this.multiple) {
          this.selected = [];
        }
        this.clearMarked();
        this.selected.push({ id: known.id, name: known.name, readonly: !!known.readonly });
        if (this.mode === 'remote' && this.multiple) {
          if (!Array.isArray(this.results)) {
            this.results = [];
          }
        } else {
          this.query = '';
        }
        if (this.mode === 'local') {
          this.results = this.localItems.slice();
        } else if (!this.multiple) {
          this.results = [];
        }
        this.updateStatus();
        this.emitFieldSync();
        return true;
      },

      removeItem: function (id) {
        if (this.readonly) {
          return;
        }
        var sid = String(id);
        if (this.isMarked(sid)) {
          this.clearMarked();
        }
        for (var i = 0; i < this.selected.length; i++) {
          if (this.selected[i].id === sid) {
            if (this.selected[i].readonly) {
              return;
            }
            if (this.removingIds[sid]) {
              return;
            }
            this.removingIds[sid] = true;
            this.emitFieldSync();
            if (this.removeTimers[sid]) {
              clearTimeout(this.removeTimers[sid]);
            }
            var self = this;
            this.removeTimers[sid] = setTimeout(function () {
              delete self.removeTimers[sid];
              for (var idx = 0; idx < self.selected.length; idx++) {
                if (self.selected[idx].id === sid) {
                  self.selected.splice(idx, 1);
                  break;
                }
              }
              delete self.removingIds[sid];
              self.updateStatus();
              self.emitFieldSync();
            }, 340);
            return;
          }
        }
      },

      removeLastEditable: function () {
        if (this.readonly) {
          return;
        }
        for (var i = this.selected.length - 1; i >= 0; i--) {
          if (!this.selected[i].readonly) {
            this.removeItem(this.selected[i].id);
            return;
          }
        }
      },

      onBackspace: function () {
        if ((this.query || '') !== '') {
          return;
        }
        var markedIndex = this.getMarkedIndex();
        if (markedIndex >= 0) {
          this.removeItem(this.selected[markedIndex].id);
          return;
        }
        var lastEditableIndex = this.getLastEditableIndex();
        if (lastEditableIndex >= 0) {
          this.markIndex(lastEditableIndex);
        }
      },

      onArrowLeft: function (event) {
        if ((this.query || '') !== '' || this.selected.length === 0) {
          return;
        }
        if (event && typeof event.preventDefault === 'function') {
          event.preventDefault();
        }
        var markedIndex = this.getMarkedIndex();
        if (markedIndex < 0) {
          this.markIndex(this.selected.length - 1);
          return;
        }
        this.markIndex(Math.max(0, markedIndex - 1));
      },

      onArrowRight: function (event) {
        if ((this.query || '') !== '' || this.selected.length === 0) {
          return;
        }
        if (event && typeof event.preventDefault === 'function') {
          event.preventDefault();
        }
        var markedIndex = this.getMarkedIndex();
        if (markedIndex < 0) {
          this.markIndex(0);
          return;
        }
        if (markedIndex >= this.selected.length - 1) {
          this.clearMarked();
          return;
        }
        this.markIndex(markedIndex + 1);
      },

      moveActive: function (step) {
        if (!this.open) {
          this.openDropdown();
          return;
        }
        var list = this.filteredResults();
        if (!list.length) {
          this.activeIndex = -1;
          return;
        }
        if (this.activeIndex < 0) {
          this.activeIndex = 0;
          return;
        }
        this.activeIndex = (this.activeIndex + step + list.length) % list.length;
      },

      selectIndex: function (idx) {
        var list = this.filteredResults();
        if (idx < 0 || idx >= list.length) {
          return;
        }
        this.addItem(list[idx]);
        this.focusInput();
      },

      selectByEnter: function () {
        if (!this.open) {
          this.openDropdown();
          return;
        }
        var list = this.filteredResults();
        if (!list.length) {
          return;
        }
        var idx = this.activeIndex;
        if (idx < 0 || idx >= list.length) {
          idx = 0;
        }
        this.selectIndex(idx);
      }
    };
  };
})();
</script>
HTML;

        $inputInner .= "\n" . $script;

        return self::renderContainer($labelHtml, $inputInner, $helpHtml, $errorHtml, $clientErr, $opts);
    }
}
