// Copyright (C) 2019 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 m from 'mithril';
import {copyToClipboard} from '../../base/clipboard';
import {assertExists} from '../../base/logging';
import {RecordConfig} from './record_config_types';
import {assetSrc} from '../../base/assets';
import {scheduleFullRedraw} from '../../widgets/raf';

export declare type Setter<T> = (cfg: RecordConfig, val: T) => void;
export declare type Getter<T> = (cfg: RecordConfig) => T;

function defaultSort(a: string, b: string) {
  return a.localeCompare(b);
}

// +---------------------------------------------------------------------------+
// | Docs link with 'i' in circle icon.                                        |
// +---------------------------------------------------------------------------+

interface DocsChipAttrs {
  href: string;
}

class DocsChip implements m.ClassComponent<DocsChipAttrs> {
  view({attrs}: m.CVnode<DocsChipAttrs>) {
    return m(
      'a.inline-chip',
      {href: attrs.href, title: 'Open docs in new tab', target: '_blank'},
      m('i.material-icons', 'info'),
      ' Docs',
    );
  }
}

// +---------------------------------------------------------------------------+
// | Probe: the rectangular box on the right-hand-side with a toggle box.      |
// +---------------------------------------------------------------------------+

export interface ProbeAttrs {
  recCfg: RecordConfig;
  title: string;
  img: string | null;
  compact?: boolean;
  descr: m.Children;
  isEnabled: Getter<boolean>;
  setEnabled: Setter<boolean>;
}

export class Probe implements m.ClassComponent<ProbeAttrs> {
  view({attrs, children}: m.CVnode<ProbeAttrs>) {
    const onToggle = (enabled: boolean) => {
      attrs.setEnabled(attrs.recCfg, enabled);
      scheduleFullRedraw();
    };

    const enabled = attrs.isEnabled(attrs.recCfg);

    return m(
      `.probe${attrs.compact ? '.compact' : ''}${enabled ? '.enabled' : ''}`,
      attrs.img &&
        m('img', {
          src: assetSrc(`assets/${attrs.img}`),
          onclick: () => onToggle(!enabled),
        }),
      m(
        'label',
        m(`input[type=checkbox]`, {
          checked: enabled,
          oninput: (e: InputEvent) => {
            onToggle((e.target as HTMLInputElement).checked);
          },
        }),
        m('span', attrs.title),
      ),
      attrs.compact
        ? ''
        : m(
            `div${attrs.img ? '' : '.extended-desc'}`,
            m('div', attrs.descr),
            m('.probe-config', children),
          ),
    );
  }
}

export function CompactProbe(args: {
  recCfg: RecordConfig;
  title: string;
  isEnabled: Getter<boolean>;
  setEnabled: Setter<boolean>;
}) {
  return m(Probe, {
    recCfg: args.recCfg,
    title: args.title,
    img: null,
    compact: true,
    descr: '',
    isEnabled: args.isEnabled,
    setEnabled: args.setEnabled,
  });
}

// +-------------------------------------------------------------+
// | Toggle: an on/off switch.
// +-------------------------------------------------------------+

export interface ToggleAttrs {
  recCfg: RecordConfig;
  title: string;
  descr: string;
  cssClass?: string;
  isEnabled: Getter<boolean>;
  setEnabled: Setter<boolean>;
}

export class Toggle implements m.ClassComponent<ToggleAttrs> {
  view({attrs}: m.CVnode<ToggleAttrs>) {
    const onToggle = (enabled: boolean) => {
      attrs.setEnabled(attrs.recCfg, enabled);
      scheduleFullRedraw();
    };

    const enabled = attrs.isEnabled(attrs.recCfg);

    return m(
      `.toggle${enabled ? '.enabled' : ''}${attrs.cssClass ?? ''}`,
      m(
        'label',
        m(`input[type=checkbox]`, {
          checked: enabled,
          oninput: (e: InputEvent) => {
            onToggle((e.target as HTMLInputElement).checked);
          },
        }),
        m('span', attrs.title),
      ),
      m('.descr', attrs.descr),
    );
  }
}

// +---------------------------------------------------------------------------+
// | Slider: draggable horizontal slider with numeric spinner.                 |
// +---------------------------------------------------------------------------+

export interface SliderAttrs {
  recCfg: RecordConfig;
  title: string;
  icon?: string;
  cssClass?: string;
  isTime?: boolean;
  unit: string;
  values: number[];
  get: Getter<number>;
  set: Setter<number>;
  min?: number;
  description?: string;
  disabled?: boolean;
  zeroIsDefault?: boolean;
}

export class Slider implements m.ClassComponent<SliderAttrs> {
  onValueChange(attrs: SliderAttrs, newVal: number) {
    attrs.set(attrs.recCfg, newVal);
    scheduleFullRedraw();
  }

  onTimeValueChange(attrs: SliderAttrs, hms: string) {
    try {
      const date = new Date(`1970-01-01T${hms}.000Z`);
      if (isNaN(date.getTime())) return;
      this.onValueChange(attrs, date.getTime());
    } catch {}
  }

  onSliderChange(attrs: SliderAttrs, newIdx: number) {
    this.onValueChange(attrs, attrs.values[newIdx]);
  }

  view({attrs}: m.CVnode<SliderAttrs>) {
    const id = attrs.title.replace(/[^a-z0-9]/gim, '_').toLowerCase();
    const maxIdx = attrs.values.length - 1;
    const val = attrs.get(attrs.recCfg);
    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
    let min = attrs.min || 1;
    if (attrs.zeroIsDefault) {
      min = Math.min(0, min);
    }
    const description = attrs.description;
    const disabled = attrs.disabled;

    // Find the index of the closest value in the slider.
    let idx = 0;
    for (; idx < attrs.values.length && attrs.values[idx] < val; idx++) {}

    let spinnerCfg = {};
    if (attrs.isTime) {
      spinnerCfg = {
        type: 'text',
        pattern: '(0[0-9]|1[0-9]|2[0-3])(:[0-5][0-9]){2}', // hh:mm:ss
        value: new Date(val).toISOString().substr(11, 8),
        oninput: (e: InputEvent) => {
          this.onTimeValueChange(attrs, (e.target as HTMLInputElement).value);
        },
      };
    } else {
      const isDefault = attrs.zeroIsDefault && val === 0;
      spinnerCfg = {
        type: 'number',
        value: isDefault ? '' : val,
        placeholder: isDefault ? '(default)' : '',
        oninput: (e: InputEvent) => {
          this.onValueChange(attrs, +(e.target as HTMLInputElement).value);
        },
      };
    }
    return m(
      '.slider' + (attrs.cssClass ?? ''),
      m('header', attrs.title),
      description ? m('header.descr', attrs.description) : '',
      attrs.icon !== undefined ? m('i.material-icons', attrs.icon) : [],
      m(`input[id="${id}"][type=range][min=0][max=${maxIdx}][value=${idx}]`, {
        disabled,
        oninput: (e: InputEvent) => {
          this.onSliderChange(attrs, +(e.target as HTMLInputElement).value);
        },
      }),
      m(`input.spinner[min=${min}][for=${id}]`, spinnerCfg),
      m('.unit', attrs.unit),
    );
  }
}

// +---------------------------------------------------------------------------+
// | Dropdown: wrapper around <select>. Supports single an multiple selection. |
// +---------------------------------------------------------------------------+

export interface DropdownAttrs {
  recCfg: RecordConfig;
  title: string;
  cssClass?: string;
  options: Map<string, string>;
  sort?: (a: string, b: string) => number;
  get: Getter<string[]>;
  set: Setter<string[]>;
}

export class Dropdown implements m.ClassComponent<DropdownAttrs> {
  resetScroll(dom: HTMLSelectElement) {
    // Chrome seems to override the scroll offset on creationa, b without this,
    // even though we call it after having marked the options as selected.
    setTimeout(() => {
      // Don't reset the scroll position if the element is still focused.
      if (dom !== document.activeElement) dom.scrollTop = 0;
    }, 0);
  }

  onChange(attrs: DropdownAttrs, e: Event) {
    const dom = e.target as HTMLSelectElement;
    const selKeys: string[] = [];
    for (let i = 0; i < dom.selectedOptions.length; i++) {
      const item = assertExists(dom.selectedOptions.item(i));
      selKeys.push(item.value);
    }
    attrs.set(attrs.recCfg, selKeys);
    scheduleFullRedraw();
  }

  view({attrs}: m.CVnode<DropdownAttrs>) {
    const options: m.Children = [];
    const selItems = attrs.get(attrs.recCfg);
    let numSelected = 0;
    const entries = [...attrs.options.entries()];
    const f = attrs.sort === undefined ? defaultSort : attrs.sort;
    entries.sort((a, b) => f(a[1], b[1]));
    for (const [key, label] of entries) {
      const opts = {value: key, selected: false};
      if (selItems.includes(key)) {
        opts.selected = true;
        numSelected++;
      }
      options.push(m('option', opts, label));
    }
    const label = `${attrs.title} ${numSelected ? `(${numSelected})` : ''}`;
    return m(
      `select.dropdown${attrs.cssClass ?? ''}[multiple=multiple]`,
      {
        onblur: (e: Event) => this.resetScroll(e.target as HTMLSelectElement),
        onmouseleave: (e: Event) =>
          this.resetScroll(e.target as HTMLSelectElement),
        oninput: (e: Event) => this.onChange(attrs, e),
        oncreate: (vnode) => this.resetScroll(vnode.dom as HTMLSelectElement),
      },
      m('optgroup', {label}, options),
    );
  }
}

// +---------------------------------------------------------------------------+
// | Textarea: wrapper around <textarea>.                                      |
// +---------------------------------------------------------------------------+

export interface TextareaAttrs {
  recCfg: RecordConfig;
  placeholder: string;
  docsLink?: string;
  cssClass?: string;
  get: Getter<string>;
  set: Setter<string>;
  title?: string;
}

export class Textarea implements m.ClassComponent<TextareaAttrs> {
  onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
    attrs.set(attrs.recCfg, dom.value);
    scheduleFullRedraw();
  }

  view({attrs}: m.CVnode<TextareaAttrs>) {
    return m(
      '.textarea-holder',
      m(
        'header',
        attrs.title,
        attrs.docsLink && [' ', m(DocsChip, {href: attrs.docsLink})],
      ),
      m(`textarea.extra-input${attrs.cssClass ?? ''}`, {
        onchange: (e: Event) =>
          this.onChange(attrs, e.target as HTMLTextAreaElement),
        placeholder: attrs.placeholder,
        value: attrs.get(attrs.recCfg),
      }),
    );
  }
}

// +---------------------------------------------------------------------------+
// | CodeSnippet: command-prompt-like box with code snippets to copy/paste.    |
// +---------------------------------------------------------------------------+

export interface CodeSnippetAttrs {
  text: string;
  hardWhitespace?: boolean;
}

export class CodeSnippet implements m.ClassComponent<CodeSnippetAttrs> {
  view({attrs}: m.CVnode<CodeSnippetAttrs>) {
    return m(
      '.code-snippet',
      m(
        'button',
        {
          title: 'Copy to clipboard',
          onclick: () => copyToClipboard(attrs.text),
        },
        m('i.material-icons', 'assignment'),
      ),
      m('code', attrs.text),
    );
  }
}

export interface CategoryGetter {
  get: Getter<string[]>;
  set: Setter<string[]>;
}

type CategoriesCheckboxListParams = CategoryGetter & {
  recCfg: RecordConfig;
  categories: Map<string, string>;
  title: string;
};

export class CategoriesCheckboxList
  implements m.ClassComponent<CategoriesCheckboxListParams>
{
  updateValue(
    attrs: CategoriesCheckboxListParams,
    value: string,
    enabled: boolean,
  ) {
    const values = attrs.get(attrs.recCfg);
    const index = values.indexOf(value);
    if (enabled && index === -1) {
      values.push(value);
    }
    if (!enabled && index !== -1) {
      values.splice(index, 1);
    }
    scheduleFullRedraw();
  }

  view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
    const enabled = new Set(attrs.get(attrs.recCfg));
    return m(
      '.categories-list',
      m(
        'h3',
        attrs.title,
        m(
          'button.config-button',
          {
            onclick: () => {
              attrs.set(attrs.recCfg, Array.from(attrs.categories.keys()));
            },
          },
          'All',
        ),
        m(
          'button.config-button',
          {
            onclick: () => {
              attrs.set(attrs.recCfg, []);
            },
          },
          'None',
        ),
      ),
      m(
        'ul.checkboxes',
        Array.from(attrs.categories.entries()).map(([key, value]) => {
          const id = `category-checkbox-${key}`;
          return m(
            'label',
            {for: id},
            m(
              'li',
              m('input[type=checkbox]', {
                id,
                checked: enabled.has(key),
                onclick: (e: InputEvent) => {
                  const target = e.target as HTMLInputElement;
                  this.updateValue(attrs, key, target.checked);
                },
              }),
              value,
            ),
          );
        }),
      ),
    );
  }
}
