import {
  WidgetSource,
  type AggregationsRequestOptions,
  type AggregationsResponse,
  type CategoryRequestOptions,
  type CategoryResponse,
  type ExtentResponse,
  type FeaturesResponse,
  type FormulaRequestOptions,
  type FormulaResponse,
  type HistogramRequestOptions,
  type HistogramResponse,
  type RangeRequestOptions,
  type RangeResponse,
  type ScatterRequestOptions,
  type ScatterResponse,
  type SpatialFilter,
  type TableRequestOptions,
  type TableResponse,
  type TilesetSourceOptions,
  type TimeSeriesRequestOptions,
  type TimeSeriesResponse,
  type WidgetSourceProps,
} from '@carto/api-client'
import type { Feature } from 'geojson'
import { Method } from './const'
import { WidgetGeoJsonSourceImpl } from './widget-geojson-source-impl'
import type { FeatureData, WorkerRequest, WorkerResponse } from './types'
import Worker from './widget-geojson-worker?worker&inline'

export type WidgetGeoJsonSourceProps = WidgetSourceProps &
  Omit<TilesetSourceOptions, 'filters' | 'tableName'> & {
    data: Feature[]
  }

export interface WidgetGeoJsonSourceResult {
  widgetSource: WidgetGeoJsonSource
}

/**
 * Source for Widget API requests on a data source defined by a geojson.
 */
export class WidgetGeoJsonSource<
  Props extends WidgetGeoJsonSourceProps = WidgetGeoJsonSourceProps,
> extends WidgetSource<Props> {
  protected _localImpl: WidgetGeoJsonSourceImpl | null = null

  protected _workerImpl: Worker | null = null
  protected _workerEnabled: boolean
  protected _workerNextRequestId = 1

  constructor(props: Props) {
    super(props)

    this._workerEnabled =
      (props.widgetWorker ?? true) && typeof Worker !== 'undefined'

    if (!this._workerEnabled) {
      this._localImpl = new WidgetGeoJsonSourceImpl(this.props)
    }
    if (props.data && Array.isArray(props.data)) {
      this.loadGeoJSON({ geojson: props.data })
    }
  }

  override destroy() {
    this._localImpl?.destroy()
    this._localImpl = null

    this._workerImpl?.terminate()
    this._workerImpl = null

    super.destroy()
  }

  /////////////////////////////////////////////////////////////////////////////
  // WEB WORKER MANAGEMENT

  /**
   * Returns an initialized Worker, to be reused for the lifecycle of this
   * source instance.
   */
  protected _getWorker(): Worker {
    if (this._workerImpl) {
      return this._workerImpl
    }

    this._workerImpl = new Worker({
      name: 'cartowidgetgeojson',
    })

    this._workerImpl.addEventListener('error', (e) => {
      // eslint-disable-next-line no-console
      console.error('widget-geojson-source worker error', e)
    })

    this._workerImpl.postMessage({
      method: Method.INIT,
      params: [this.props],
    } as WorkerRequest)

    return this._workerImpl
  }

  /** Executes a given method on the worker. */
  protected _executeWorkerMethod<T>(
    method: (typeof Method)[keyof typeof Method],
    params: unknown[],
    signal?: AbortSignal,
  ): Promise<T> {
    if (!this._workerEnabled) {
      if (!this._localImpl) {
        return Promise.reject(new Error('Local implementation not initialized'))
      }
      // @ts-expect-error - Dynamic method call with runtime type checking
      // eslint-disable-next-line @typescript-eslint/no-unsafe-call
      return this._localImpl[method](...params) as Promise<T>
    }

    const worker = this._getWorker()
    const requestId = this._workerNextRequestId++

    let resolve: ((value: T) => void) | null = null
    let reject: ((reason: unknown) => void) | null = null

    // If worker sends message to main process, check whether it's a response
    // to this request, and whether the request can been aborted. Then resolve
    // or reject the Promise.
    function onMessage(e: MessageEvent) {
      const response = e.data as WorkerResponse
      if (response.requestId !== requestId) return
      if (signal?.aborted) return // handled by 'abort' listener

      if (response.ok) {
        resolve!(response.result as T)
      } else {
        reject!(new Error(response.error))
      }
    }

    // If request is aborted by user, immediately reject the Promise.
    function onAbort() {
      reject!(new Error(String(signal!.reason)))
    }

    worker.addEventListener('message', onMessage)
    signal?.addEventListener('abort', onAbort)

    // Send the task to the worker, creating a Promise to resolve/reject later.
    const promise = new Promise<T>((_resolve, _reject) => {
      resolve = _resolve
      reject = _reject

      worker.postMessage({
        requestId,
        method,
        params,
      } as WorkerRequest)
    })

    // Whether the task completes, fails, or aborts: clean up afterward.
    void promise.finally(() => {
      worker.removeEventListener('message', onMessage)
      signal?.removeEventListener('abort', onAbort)
    })

    return promise
  }

  /////////////////////////////////////////////////////////////////////////////
  // DATA LOADING

  /**
   * Loads features as GeoJSON
   */
  loadGeoJSON({ geojson }: { geojson: FeatureData[] }) {
    if (!this._workerEnabled) {
      return this._localImpl!.loadGeoJSON({ geojson })
    }

    const worker = this._getWorker()

    worker.postMessage({
      method: Method.LOAD_GEOJSON,
      params: [{ geojson }],
    } as WorkerRequest)
  }

  /////////////////////////////////////////////////////////////////////////////
  // WIDGETS API

  // eslint-disable-next-line @typescript-eslint/require-await
  override async getFeatures(): Promise<FeaturesResponse> {
    throw new Error('getFeatures not supported for geojson')
  }

  async getFormula({
    signal,
    ...options
  }: FormulaRequestOptions): Promise<FormulaResponse> {
    return this._executeWorkerMethod(Method.GET_FORMULA, [options], signal)
  }

  override async getHistogram({
    signal,
    ...options
  }: HistogramRequestOptions): Promise<HistogramResponse> {
    return this._executeWorkerMethod(Method.GET_HISTOGRAM, [options], signal)
  }

  override async getCategories({
    signal,
    ...options
  }: CategoryRequestOptions): Promise<CategoryResponse> {
    return this._executeWorkerMethod(Method.GET_CATEGORIES, [options], signal)
  }

  override async getScatter({
    signal,
    ...options
  }: ScatterRequestOptions): Promise<ScatterResponse> {
    return this._executeWorkerMethod(Method.GET_SCATTER, [options], signal)
  }

  override async getTable({
    signal,
    ...options
  }: TableRequestOptions): Promise<TableResponse> {
    return this._executeWorkerMethod(Method.GET_TABLE, [options], signal)
  }

  override async getTimeSeries({
    signal,
    ...options
  }: TimeSeriesRequestOptions): Promise<TimeSeriesResponse> {
    return this._executeWorkerMethod(Method.GET_TIME_SERIES, [options], signal)
  }

  override async getRange({
    signal,
    ...options
  }: RangeRequestOptions): Promise<RangeResponse> {
    return this._executeWorkerMethod(Method.GET_RANGE, [options], signal)
  }

  async getAggregations({
    signal,
    ...options
  }: AggregationsRequestOptions): Promise<AggregationsResponse> {
    return this._executeWorkerMethod(Method.GET_AGGREGATIONS, [options], signal)
  }

  /** @experimental */
  async getExtent({
    signal,
    ...options
  }: {
    signal?: AbortSignal
    spatialFilter?: SpatialFilter
  }): Promise<ExtentResponse> {
    return this._executeWorkerMethod<ExtentResponse>(
      Method.GET_EXTENT,
      [options],
      signal,
    )
  }
}
