/* eslint-disable @typescript-eslint/no-namespace */

import React, { useRef, useEffect, useImperativeHandle } from 'react';
import mapboxgl from 'mapbox-gl';

import {
  GeocodingOptions,
  GeocodingResponse,
  GeocodingFeature
} from '@mapbox/search-js-core';
import {
  MapboxGeocoder,
  Theme,
  MapboxHTMLEvent,
  PopoverOptions
} from '@mapbox/search-js-web';

declare global {
  namespace JSX {
    interface IntrinsicElements {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      'mapbox-geocoder': any;
    }
  }
}

/**
 * @typedef GeocoderRefType
 */
export interface GeocoderRefType {
  /**
   * @see {@link MapboxGeocoder#focus}
   */
  focus: typeof MapboxGeocoder.prototype.focus;
  /**
   * @see {@link MapboxGeocoder#search}
   */
  search: typeof MapboxGeocoder.prototype.search;
}

/**
 * @typedef GeocoderProps
 */
export interface GeocoderProps {
  /**
   * The [Mapbox access token](https://docs.mapbox.com/help/glossary/access-token/) to use for all requests.
   */
  accessToken: string;
  /**
   * Options to pass to the underlying {@link GeocodingCore} interface.
   * @example
   * ```typescript
   * <Geocoder options={{
   *  language: 'en',
   *  country: 'US',
   * }}>
   * ```
   */
  options?: Partial<GeocodingOptions>;
  /**
   * The {@link Theme} to use for styling the geocoder.
   * @example
   * ```typescript
   * <Geocoder theme={{
   *   variables: {
   *     colorPrimary: 'myBrandRed'
   *   }
   * }}>
   * ```
   */
  theme?: Theme;
  /**
   * The {@link PopoverOptions} to define popover positioning.
   * @example
   * ```typescript
   * <Geocoder popoverOptions={{
   *   placement: 'top-start',
   *   flip: true,
   *   offset: 5
   * }}>
   * ```
   */
  popoverOptions?: Partial<PopoverOptions>;
  /**
   * The input element's placeholder text. The default value may be localized if {@link GeocodingOptions#language} is set.
   */
  placeholder?: string;
  /**
   * If specified, the map will be centered on the retrieved suggestion.
   */
  map?: mapboxgl.Map;
  /**
   * If `true`, a [Marker](https://docs.mapbox.com/mapbox-gl-js/api/#marker) will be added to the map at the location of the user-selected result using a default set of Marker options.  If the value is an object, the marker will be constructed using these options. If `false`, no marker will be added to the map. Requires that {@link GeocoderProps#mapboxgl} also be set.
   */
  marker?: boolean | mapboxgl.MarkerOptions;
  /**
   * A [mapbox-gl](https://github.com/mapbox/mapbox-gl-js) instance to use when creating [Markers](https://docs.mapbox.com/mapbox-gl-js/api/#marker). Required if {@link GeocoderProps#marker} is `true`.
   */
  mapboxgl?: typeof mapboxgl;
  /**
   * Value to display in the geocoder.
   */
  value?: string;
  /**
   * Callback for when the value changes.
   */
  onChange?: (value: string) => void;
  /**
   * Fired when the user is typing in the input and provides a list of suggestions.
   * The underlying response from {@link GeocodingCore} is passed.
   */
  onSuggest?: (res: GeocodingResponse) => void;
  /**
   * Fired when {@link GeocodingCore} has errored providing a list of suggestions.
   * The underlying error is passed.
   */
  onSuggestError?: (error: Error) => void;
  /**
   * Fired when the user has selected a suggestion.
   * The underlying feature from {@link GeocodingCore} is passed.
   */
  onRetrieve?: (res: GeocodingFeature) => void;
  /**
   * Fired when the user has cleared the search box.
   */
  onClear?: () => void;

  /**
   * A callback providing the opportunity to validate and/or manipulate the input text before it triggers a search, for example by using a regular expression.
   * If a truthy string value is returned, it will be passed into the underlying search API. If `null`, `undefined` or empty string  is returned, no search request will be performed.
   */
  interceptSearch?: (value: string) => string;
}

/**
 * `<Geocoder>` is a React component that provides an interactive geocoder,
 * powered by the Mapbox Geocoding API.
 *
 * To use this element, you must have a [Mapbox access token](https://www.mapbox.com/help/create-api-access-token/).
 *
 * Internally, this wraps the [`<mapbox-geocoder>`](https://docs.mapbox.com/mapbox-search-js/api/web/search/#mapboxgeocoder) element.
 *
 * @function Geocoder
 * @param {GeocoderProps} props
 * @example
 * ```typescript
 * import { Geocoder } from "@mapbox/search-js-react";
 * export function Component() {
 *   const [value, setValue] = React.useState('');
 *
 *   const handleChange = (d) => {
 *     setValue(d);
 *   };
 *   return (
 *     <Geocoder
 *       options={{
 *         proximity: {
 *           lng: -122.431297,
 *           lat: 37.773972,
 *         },
 *       }}
 *       value={value}
 *       onChange={handleChange}
 *       accessToken="YOUR_MAPBOX_ACCESS_TOKEN"
 *     />
 *   );
 * }
 * ```
 */
export const Geocoder = React.forwardRef(
  (props: GeocoderProps, refProp): React.ReactElement => {
    const {
      accessToken,
      options,
      theme,
      popoverOptions,
      placeholder,
      map,
      marker,
      mapboxgl,
      value,
      onChange,
      onSuggest,
      onSuggestError,
      onRetrieve,
      onClear,
      interceptSearch
    } = props;
    const ref = useRef<MapboxGeocoder>();

    useImperativeHandle(refProp, () => ({
      focus: () => {
        if (ref.current) return ref.current.focus();
        throw new Error('Geocoder is not mounted');
      },
      search: (text: string) => {
        if (ref.current) return ref.current.search(text);
        throw new Error('Geocoder is not mounted');
      }
    }));

    // Update options.
    useEffect(() => {
      if (ref.current) ref.current.options = options || {};
    }, [ref.current, options]);

    // Update intercept search.
    useEffect(() => {
      if (ref.current) ref.current.interceptSearch = interceptSearch;
    }, [ref.current, interceptSearch]);

    // Update theme.
    useEffect(() => {
      if (ref.current) ref.current.theme = theme;
    }, [ref.current, theme]);

    // Update popoverOptions
    useEffect(() => {
      if (ref.current) ref.current.popoverOptions = popoverOptions;
    }, [ref.current, popoverOptions]);

    // Update placeholder
    useEffect(() => {
      if (ref.current) ref.current.placeholder = placeholder;
    }, [ref.current, placeholder]);

    // Update value.
    useEffect(() => {
      if (ref.current) ref.current.value = value || '';
    }, [ref.current, value]);

    // Update map.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      node.bindMap(map);
      return () => {
        node.unbindMap();
      };
    }, [ref.current, map]);

    // Update marker.
    useEffect(() => {
      if (ref.current) ref.current.marker = marker;
    }, [ref.current, marker]);

    // Update mapboxgl.
    useEffect(() => {
      if (ref.current) ref.current.mapboxgl = mapboxgl;
    }, [ref.current, mapboxgl]);

    // Update onSuggest.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      if (!onSuggest) return;

      const fn = (e: MapboxHTMLEvent<GeocodingResponse>) => onSuggest(e.detail);

      node.addEventListener('suggest', fn);
      return () => {
        node.removeEventListener('suggest', fn);
      };
    }, [ref.current, onSuggest]);

    // Update onSuggestError.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      if (!onSuggestError) return;

      const fn = (e: MapboxHTMLEvent<Error>) => onSuggestError(e.detail);

      node.addEventListener('suggesterror', fn);
      return () => {
        node.removeEventListener('suggesterror', fn);
      };
    }, [ref.current, onSuggestError]);

    // Update onRetrieve.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      if (!onRetrieve) return;

      const fn = (e: MapboxHTMLEvent<GeocodingFeature>) => onRetrieve(e.detail);

      node.addEventListener('retrieve', fn);
      return () => {
        node.removeEventListener('retrieve', fn);
      };
    }, [ref.current, onRetrieve]);

    // Update onChange.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      if (!onChange) return;

      const fn = (e: MapboxHTMLEvent<string>) => {
        if (e.target !== e.currentTarget) return; // ignore child input event
        onChange(e.detail);
      };

      node.addEventListener('input', fn);
      return () => {
        node.removeEventListener('input', fn);
      };
    }, [ref.current, onChange]);

    // Update onClear.
    useEffect(() => {
      const node = ref.current;
      if (!node) return;

      if (!onClear) return;

      const fn = () => {
        onClear();
      };

      node.addEventListener('clear', fn);
      return () => {
        node.removeEventListener('clear', fn);
      };
    }, [ref.current, onClear]);

    // Update accessToken.
    useEffect(() => {
      if (ref.current) ref.current.accessToken = accessToken;
    }, [ref.current, accessToken]);

    return <mapbox-geocoder ref={ref} />;
  }
);
