// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import './multiselect.scss';
import m from 'mithril';
import {Icons} from '../base/semantic_icons';
import {Button} from './button';
import {Checkbox} from './checkbox';
import {EmptyState} from './empty_state';
import {Popup, PopupPosition} from './popup';
import {TextInput} from './text_input';
import type {Intent} from './common';

export interface MultiSelectOption {
  // The ID is used to indentify this option, and is used in callbacks.
  id: string;
  // This is the name used for searching, and displayed if `label` is not
  // provided.
  name: string;
  // Whether the option is selected or not.
  checked: boolean;
  // If provided, this is used as checkbox the label.
  label?: m.Children;
  // Optional details that are shown on hover.
  details?: string;
}

export interface MultiSelectDiff {
  id: string;
  checked: boolean;
}

export interface MultiSelectAttrs {
  options: MultiSelectOption[];
  onChange?: (diffs: MultiSelectDiff[]) => void;
  repeatCheckedItemsAtTop?: boolean;
  showNumSelected?: boolean;
  fixedSize?: boolean;
  readonly showSelectAllButton?: boolean;
}

export type PopupMultiSelectAttrs = MultiSelectAttrs & {
  intent?: Intent;
  compact?: boolean;
  icon?: string;
  label: string;
  position?: PopupPosition;
};

// A component which shows a list of items with checkboxes, allowing the user to
// select from the list which ones they want to be selected.
// Also provides search functionality.
// This component is entirely controlled and callbacks must be supplied for when
// the selected items list changes, and when the search term changes.
// There is an optional boolean flag to enable repeating the selected items at
// the top of the list for easy access - defaults to false.
export class MultiSelect implements m.ClassComponent<MultiSelectAttrs> {
  private searchText: string = '';

  view({attrs}: m.CVnode<MultiSelectAttrs>) {
    const {options, fixedSize = true} = attrs;
    this.validateOptions(options);

    const filteredItems = options.filter(({name}) => {
      return name.toLowerCase().includes(this.searchText.toLowerCase());
    });

    return m(
      fixedSize
        ? '.pf-multiselect-panel.pf-multi-select-fixed-size'
        : '.pf-multiselect-panel',
      this.renderSearchBox(),
      this.renderListOfItems(attrs, filteredItems),
    );
  }

  private validateOptions(options: MultiSelectOption[]) {
    const ids = new Set<string>();
    for (const option of options) {
      if (ids.has(option.id)) {
        throw new Error(`Duplicate option ID: ${option.id}`);
      }
      ids.add(option.id);
    }
  }

  private renderListOfItems(
    attrs: MultiSelectAttrs,
    options: MultiSelectOption[],
  ) {
    const {
      repeatCheckedItemsAtTop,
      onChange = () => {},
      showSelectAllButton = true,
    } = attrs;
    const allChecked = options.every(({checked}) => checked);
    const anyChecked = options.some(({checked}) => checked);

    if (options.length === 0) {
      return m(EmptyState, {
        title: `No results for '${this.searchText}'`,
      });
    } else {
      return [
        m(
          '.pf-list',
          repeatCheckedItemsAtTop &&
            anyChecked &&
            m(
              '.pf-multiselect-container',
              m(
                '.pf-multiselect-header',
                m(
                  'span',
                  this.searchText === '' ? 'Selected' : `Selected (Filtered)`,
                ),
                m(Button, {
                  label:
                    this.searchText === '' ? 'Clear All' : 'Clear Filtered',
                  icon: Icons.Deselect,
                  onclick: () => {
                    const diffs = options
                      .filter(({checked}) => checked)
                      .map(({id}) => ({id, checked: false}));
                    onChange(diffs);
                  },
                  disabled: !anyChecked,
                }),
              ),
              this.renderOptions(
                attrs,
                options.filter(({checked}) => checked),
              ),
            ),
          m(
            '.pf-multiselect-container',
            m(
              '.pf-multiselect-header',
              m(
                'span',
                this.searchText === '' ? 'Options' : `Options (Filtered)`,
              ),
              showSelectAllButton &&
                m(Button, {
                  label:
                    this.searchText === '' ? 'Select All' : 'Select Filtered',
                  icon: Icons.SelectAll,
                  compact: true,
                  onclick: () => {
                    const diffs = options
                      .filter(({checked}) => !checked)
                      .map(({id}) => ({id, checked: true}));
                    onChange(diffs);
                  },
                  disabled: allChecked,
                }),
              m(Button, {
                label: this.searchText === '' ? 'Clear All' : 'Clear Filtered',
                icon: Icons.Deselect,
                compact: true,
                onclick: () => {
                  const diffs = options
                    .filter(({checked}) => checked)
                    .map(({id}) => ({id, checked: false}));
                  onChange(diffs);
                },
                disabled: !anyChecked,
              }),
            ),
            this.renderOptions(attrs, options),
          ),
        ),
      ];
    }
  }

  private renderSearchBox() {
    return m(
      '.pf-search-bar',
      m(TextInput, {
        oninput: (event: Event) => {
          const eventTarget = event.target as HTMLTextAreaElement;
          this.searchText = eventTarget.value;
        },
        leftIcon: Icons.Search,
        value: this.searchText,
        placeholder: 'Filter options...',
        className: 'pf-search-box',
      }),
      this.renderClearButton(),
    );
  }

  private renderClearButton() {
    if (this.searchText != '') {
      return m(Button, {
        onclick: () => {
          this.searchText = '';
        },
        label: '',
        icon: 'close',
      });
    } else {
      return null;
    }
  }

  private renderOptions(attrs: MultiSelectAttrs, options: MultiSelectOption[]) {
    const {onChange = () => {}} = attrs;

    return options.map((item) => {
      const {checked, name, label, details, id} = item;
      return m(Checkbox, {
        label: label != undefined ? label : name,
        key: id, // Prevents transitions jumping between items when searching
        checked,
        className: 'pf-multiselect-item',
        onchange: () => {
          onChange([{id, checked: !checked}]);
        },
        title: details,
      });
    });
  }
}

// The same multi-select component that functions as a drop-down instead of
// a list.
export class PopupMultiSelect
  implements m.ClassComponent<PopupMultiSelectAttrs>
{
  view({attrs}: m.CVnode<PopupMultiSelectAttrs>) {
    const {icon, position = PopupPosition.Auto, intent, compact} = attrs;

    return m(
      Popup,
      {
        trigger: m(Button, {
          label: this.labelText(attrs),
          icon,
          intent,
          compact,
        }),
        position,
      },
      m(MultiSelect, attrs as MultiSelectAttrs),
    );
  }

  private labelText(attrs: PopupMultiSelectAttrs): string {
    const {options, showNumSelected, label} = attrs;

    if (showNumSelected) {
      const numSelected = options.filter(({checked}) => checked).length;
      return `${label} (${numSelected} selected)`;
    } else {
      return label;
    }
  }
}
