// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import '../../ui/kit/kit.js';

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Buttons from '../../ui/components/buttons/buttons.js';
import * as UI from '../../ui/legacy/legacy.js';
import {html, type LitTemplate, render} from '../../ui/lit/lit.js';
import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';

import locationsSettingsTabStyles from './locationsSettingsTab.css.js';

const UIStrings = {
  /**
   * @description Title in the Locations settings tab, where custom geographic locations that the user
   * has entered are stored.
   */
  locations: 'Locations',
  /**
   * @description Label for the name of a geographic location that the user has entered.
   */
  locationName: 'Location name',
  /**
   * @description Abbreviation of latitude in the Locations settings tab of the Device toolbar.
   */
  lat: 'Lat',
  /**
   * @description Abbreviation of longitude in the Locations settings tab of the Device toolbar.
   */
  long: 'Long',
  /**
   * @description Text in the Sensors view of the Device toolbar.
   */
  timezoneId: 'Timezone ID',
  /**
   * @description Label for text input for the locale of a particular location.
   */
  locale: 'Locale',
  /**
   * @description Label for text input for the latitude of a GPS position.
   */
  latitude: 'Latitude',
  /**
   * @description Label for text input for the longitude of a GPS position.
   */
  longitude: 'Longitude',
  /**
   * @description Label for text input for the accuracy of a GPS position.
   */
  accuracy: 'Accuracy',
  /**
   * @description Error message in the Locations settings tab that declares the location name input must not be empty.
   */
  locationNameCannotBeEmpty: 'Location name can’t be empty',
  /**
   * @description Error message in the Locations settings tab that declares the maximum length of the location name.
   * @example {50} PH1
   */
  locationNameMustBeLessThanS: 'Location name must be less than {PH1} characters',
  /**
   * @description Error message in the Locations settings tab that declares that the value for the latitude input must be a number.
   */
  latitudeMustBeANumber: 'Latitude must be a number',
  /**
   * @description Error message in the Locations settings tab that declares the minimum value for the latitude input.
   * @example {-90} PH1
   */
  latitudeMustBeGreaterThanOrEqual: 'Latitude must be greater than or equal to {PH1}',
  /**
   * @description Error message in the Locations settings tab that declares the maximum value for the latitude input.
   * @example {90} PH1
   */
  latitudeMustBeLessThanOrEqualToS: 'Latitude must be less than or equal to {PH1}',
  /**
   * @description Error message in the Locations settings tab that declares that the value for the longitude input must be a number.
   */
  longitudeMustBeANumber: 'Longitude must be a number',
  /**
   * @description Error message in the Locations settings tab that declares the minimum value for the longitude input.
   * @example {-180} PH1
   */
  longitudeMustBeGreaterThanOr: 'Longitude must be greater than or equal to {PH1}',
  /**
   * @description Error message in the Locations settings tab that declares the maximum value for the longitude input.
   * @example {180} PH1
   */
  longitudeMustBeLessThanOrEqualTo: 'Longitude must be less than or equal to {PH1}',
  /**
   * @description Error message in the Locations settings tab that declares timezone ID input invalid.
   */
  timezoneIdMustContainAlphabetic: 'Timezone ID must contain alphabetic characters',
  /**
   * @description Error message in the Locations settings tab that declares locale input invalid.
   */
  localeMustContainAlphabetic: 'Locale must contain alphabetic characters',
  /**
   * @description Error message in the Locations settings tab that declares that the value for the accuracy input must be a number.
   */
  accuracyMustBeANumber: 'Accuracy must be a number',
  /**
   * @description Error message in the Locations settings tab that declares the minimum value for the accuracy input.
   * @example {0} PH1
   */
  accuracyMustBeGreaterThanOrEqual: 'Accuracy must be greater than or equal to {PH1}',
  /**
   * @description Text of add locations button in the Locations settings tab of the Device toolbar.
   */
  addLocation: 'Add location',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/sensors/LocationsSettingsTab.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

function renderItemView(location: LocationDescription): LitTemplate {
  // clang-format off
  return html`
    <div class="locations-list-item" role="row">
      <div class="locations-list-text locations-list-title" role="cell">
        <div class="locations-list-title-text" title=${location.title}>${location.title}</div>
      </div>
      <div class="locations-list-separator"></div>
      <div class="locations-list-text" role="cell">${location.lat}</div>
      <div class="locations-list-separator"></div>
      <div class="locations-list-text" role="cell">${location.long}</div>
      <div class="locations-list-separator"></div>
      <div class="locations-list-text" role="cell">${location.timezoneId}</div>
      <div class="locations-list-separator"></div>
      <div class="locations-list-text" role="cell">${location.locale}</div>
      <div class="locations-list-separator"></div>
      <div class="locations-list-text" role="cell">${
          location.accuracy ?? SDK.EmulationModel.Location.DEFAULT_ACCURACY}</div>
    </div>`;
  // clang-format on
}

interface EditorInputControls {
  titleInput: Element;
  latInput: Element;
  longInput: Element;
  timezoneIdInput: Element;
  localeInput: Element;
  accuracyInput: Element;
}

function renderEditorView(controls: EditorInputControls): LitTemplate {
  // clang-format off
  return html`
    <div class="locations-edit-row">
      <div class="locations-list-text locations-list-title">${i18nString(UIStrings.locationName)}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text">${i18nString(UIStrings.lat)}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text">${i18nString(UIStrings.long)}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text">${i18nString(UIStrings.timezoneId)}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text">${i18nString(UIStrings.locale)}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text">${i18nString(UIStrings.accuracy)}</div>
    </div>
    <div class="locations-edit-row">
      <div class="locations-list-text locations-list-title locations-input-container">${controls.titleInput}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text locations-input-container">${controls.latInput}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text locations-list-text-longitude locations-input-container">${controls.longInput}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text locations-input-container">${controls.timezoneIdInput}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text locations-input-container">${controls.localeInput}</div>
      <div class="locations-list-separator locations-list-separator-invisible"></div>
      <div class="locations-list-text locations-input-container">${controls.accuracyInput}</div>
    </div>`;
  // clang-format on
}

interface LocationsViewInput {
  onAddLocation: () => void;
}

export type ViewOutput = undefined;

export const DEFAULT_VIEW = (input: LocationsViewInput, _output: ViewOutput, target: HTMLElement): void => {
  // clang-format off
  render(html`
    <style>${locationsSettingsTabStyles}</style>
    <div class="settings-card-container-wrapper">
      <div class="settings-card-container">
        <devtools-card .heading=${i18nString(UIStrings.locations)}>
          <div class="list-container"></div>
          <devtools-button
            class="add-locations-button"
            .variant=${Buttons.Button.Variant.OUTLINED}
            .iconName=${'plus'}
            .jslogContext=${'emulation.add-location'}
            @click=${input.onAddLocation}>
            ${i18nString(UIStrings.addLocation)}
          </devtools-button>
        </devtools-card>
      </div>
    </div>`, target);
  // clang-format on
};

export type View = typeof DEFAULT_VIEW;
export class LocationsSettingsTab extends UI.Widget.VBox implements UI.ListWidget.Delegate<LocationDescription> {
  private readonly list: UI.ListWidget.ListWidget<LocationDescription>;
  private readonly customSetting: Common.Settings.Setting<LocationDescription[]>;
  private editor?: UI.ListWidget.Editor<LocationDescription>;
  #view: View;

  constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
    super(element, {
      jslog: `${VisualLogging.pane('emulation-locations')}`,
      useShadowDom: true,
    });
    this.#view = view;

    this.list = new UI.ListWidget.ListWidget(this, undefined, true);
    this.list.element.classList.add('locations-list');
    this.list.registerRequiredCSS(locationsSettingsTabStyles);
    this.customSetting =
        Common.Settings.Settings.instance().moduleSetting<LocationDescription[]>('emulation.locations');
    const list =
        this.customSetting.get().map(location => replaceLocationTitles(location, this.customSetting.defaultValue));

    function replaceLocationTitles(
        location: LocationDescription, defaultValues: LocationDescription[]): LocationDescription {
      // This check is done for locations that might had been cached wrongly due to crbug.com/1171670.
      // Each of the default values would have been stored without a title if the user had added a new location
      // while the bug was present in the application. This means that getting the setting's default value with the `get`
      // method would return the default locations without a title. To cope with this, the setting values are
      // preemptively checked and corrected so that any default value mistakenly stored without a title is replaced
      // with the corresponding declared value in the pre-registered setting.
      if (!location.title) {
        const replacement = defaultValues.find(
            defaultLocation => defaultLocation.lat === location.lat && defaultLocation.long === location.long &&
                defaultLocation.timezoneId === location.timezoneId && defaultLocation.locale === location.locale);
        if (!replacement) {
          console.error('Could not determine a location setting title');
        } else {
          return replacement;
        }
      }
      return location;
    }

    this.customSetting.set(list);
    this.customSetting.addChangeListener(this.locationsUpdated, this);
  }

  override wasShown(): void {
    super.wasShown();
    this.locationsUpdated();
  }

  override performUpdate(): void {
    const viewInput = {
      onAddLocation: () => this.addButtonClicked(),
    };
    this.#view(viewInput, undefined, this.contentElement as HTMLElement);

    const listContainer = this.contentElement.querySelector('.list-container');
    if (listContainer) {
      this.list.show(listContainer);
    }
  }
  private locationsUpdated(): void {
    this.list.clear();

    const conditions = this.customSetting.get();
    for (const condition of conditions) {
      this.list.appendItem(condition, true);
    }

    this.list.appendSeparator();
    this.requestUpdate();
  }

  private addButtonClicked(): void {
    this.list.addNewItem(this.customSetting.get().length, {
      title: '',
      lat: 0,
      long: 0,
      timezoneId: '',
      locale: '',
      accuracy: SDK.EmulationModel.Location.DEFAULT_ACCURACY,
    });
  }

  renderItem(location: LocationDescription, _editable: boolean): Element {
    const fragment = document.createDocumentFragment();
    // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
    render(renderItemView(location), fragment);
    return fragment.firstElementChild as Element;
  }

  removeItemRequested(_item: LocationDescription, index: number): void {
    const list = this.customSetting.get();
    list.splice(index, 1);
    this.customSetting.set(list);
  }

  commitEdit(location: LocationDescription, editor: UI.ListWidget.Editor<LocationDescription>, isNew: boolean): void {
    location.title = editor.control('title').value.trim();
    const lat = editor.control('lat').value.trim();
    location.lat = lat ? parseFloat(lat) : 0;
    const long = editor.control('long').value.trim();
    location.long = long ? parseFloat(long) : 0;
    const timezoneId = editor.control('timezone-id').value.trim();
    location.timezoneId = timezoneId;
    const locale = editor.control('locale').value.trim();
    location.locale = locale;
    const accuracy = editor.control('accuracy').value.trim();
    location.accuracy = accuracy ? parseFloat(accuracy) : SDK.EmulationModel.Location.DEFAULT_ACCURACY;

    const list = this.customSetting.get();
    if (isNew) {
      list.push(location);
    }
    this.customSetting.set(list);
  }

  beginEdit(location: LocationDescription): UI.ListWidget.Editor<LocationDescription> {
    const editor = this.createEditor();
    editor.control('title').value = location.title;
    editor.control('lat').value = String(location.lat);
    editor.control('long').value = String(location.long);
    editor.control('timezone-id').value = location.timezoneId;
    editor.control('locale').value = location.locale;
    editor.control('accuracy').value = String(location.accuracy ?? SDK.EmulationModel.Location.DEFAULT_ACCURACY);
    return editor;
  }

  private createEditor(): UI.ListWidget.Editor<LocationDescription> {
    if (this.editor) {
      return this.editor;
    }

    const editor = new UI.ListWidget.Editor<LocationDescription>();
    this.editor = editor;
    const content = editor.contentElement();

    const createValidator = (validator: (value: string) => string | null) =>
        (_item: LocationDescription, _index: number,
         input: UI.ListWidget.EditorControl): UI.ListWidget.ValidatorResult => {
          const errorMessage = validator(input.value);
          if (errorMessage) {
            return {valid: false, errorMessage};
          }
          return {valid: true};
        };

    const titleInput =
        editor.createInput('title', 'text', i18nString(UIStrings.locationName), createValidator(validateTitle));
    const latInput =
        editor.createInput('lat', 'text', i18nString(UIStrings.latitude), createValidator(validateLatitude));
    const longInput =
        editor.createInput('long', 'text', i18nString(UIStrings.longitude), createValidator(validateLongitude));
    const timezoneIdInput = editor.createInput('timezone-id', 'text', i18nString(UIStrings.timezoneId),
                                               createValidator(validateTimezoneId));
    const localeInput =
        editor.createInput('locale', 'text', i18nString(UIStrings.locale), createValidator(validateLocale));
    const accuracyInput =
        editor.createInput('accuracy', 'text', i18nString(UIStrings.accuracy), createValidator(validateAccuracy));

    // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
    render(renderEditorView({
             titleInput,
             latInput,
             longInput,
             timezoneIdInput,
             localeInput,
             accuracyInput,
           }),
           content as HTMLElement);

    return editor;
  }
}
export interface LocationDescription {
  title: string;
  lat: number;
  long: number;
  timezoneId: string;
  locale: string;
  accuracy?: number;
}

export function validateTitle(value: string): string|null {
  const maxLength = 50;
  const trimmedValue = value.trim();

  if (!trimmedValue.length) {
    return i18nString(UIStrings.locationNameCannotBeEmpty);
  }
  if (trimmedValue.length > maxLength) {
    return i18nString(UIStrings.locationNameMustBeLessThanS, {PH1: maxLength});
  }
  return null;
}

export function validateLatitude(value: string): string|null {
  const minLat = -90;
  const maxLat = 90;
  const trimmedValue = value.trim();
  const parsedValue = Number(trimmedValue);

  if (!trimmedValue) {
    return null;
  }
  if (Number.isNaN(parsedValue)) {
    return i18nString(UIStrings.latitudeMustBeANumber);
  }
  if (parsedValue < minLat) {
    return i18nString(UIStrings.latitudeMustBeGreaterThanOrEqual, {PH1: minLat});
  }
  if (parsedValue > maxLat) {
    return i18nString(UIStrings.latitudeMustBeLessThanOrEqualToS, {PH1: maxLat});
  }
  return null;
}

export function validateLongitude(value: string): string|null {
  const minLong = -180;
  const maxLong = 180;
  const trimmedValue = value.trim();
  const parsedValue = Number(trimmedValue);

  if (!trimmedValue) {
    return null;
  }
  if (Number.isNaN(parsedValue)) {
    return i18nString(UIStrings.longitudeMustBeANumber);
  }
  if (parsedValue < minLong) {
    return i18nString(UIStrings.longitudeMustBeGreaterThanOr, {PH1: minLong});
  }
  if (parsedValue > maxLong) {
    return i18nString(UIStrings.longitudeMustBeLessThanOrEqualTo, {PH1: maxLong});
  }
  return null;
}

export function validateTimezoneId(value: string): string|null {
  const trimmedValue = value.trim();
  // Chromium uses ICU's timezone implementation, which is very
  // liberal in what it accepts. ICU does not simply use an allowlist
  // but instead tries to make sense of the input, even for
  // weird-looking timezone IDs. There's not much point in validating
  // the input other than checking if it contains at least one
  // alphabetic character. The empty string resets the override,
  // and is accepted as well.
  if (trimmedValue === '' || /[a-zA-Z]/.test(trimmedValue)) {
    return null;
  }
  return i18nString(UIStrings.timezoneIdMustContainAlphabetic);
}

export function validateLocale(value: string): string|null {
  const trimmedValue = value.trim();
  // Similarly to timezone IDs, there's not much point in validating
  // input locales other than checking if it contains at least two
  // alphabetic characters.
  // https://unicode.org/reports/tr35/#Unicode_language_identifier
  // The empty string resets the override, and is accepted as
  // well.
  if (trimmedValue === '' || /[a-zA-Z]{2}/.test(trimmedValue)) {
    return null;
  }
  return i18nString(UIStrings.localeMustContainAlphabetic);
}

export function validateAccuracy(value: string): string|null {
  const minAccuracy = 0;
  const trimmedValue = value.trim();
  const parsedValue = Number(trimmedValue);

  if (!trimmedValue) {
    return null;
  }
  if (Number.isNaN(parsedValue)) {
    return i18nString(UIStrings.accuracyMustBeANumber);
  }
  if (parsedValue < minAccuracy) {
    return i18nString(UIStrings.accuracyMustBeGreaterThanOrEqual, {PH1: minAccuracy});
  }
  return null;
}
