import type { Class } from 'type-fest'
import type { Layer as DeckLayer, LayerProps } from '@deck.gl/core'
import { TileLayer } from '@deck.gl/geo-layers'
import type { Layer } from './types'
import { isEmptyGeometrySourceError } from './is-empty-geometry-source-error'

// deck.gl props vary per Layer subclass; allow subclass-specific props (e.g.
// TileLayer's `refinementStrategy`) while keeping core props typed.
type DefaultProps = Partial<LayerProps> & Record<string, unknown>

/**
 * Internal registry of library-level default props per deck.gl Layer class.
 * Not part of the public API — seed it here so the defaults don't have to be
 * repeated in every `setLayer` call. Subclasses inherit a parent's entry (see
 * {@link withLayerDefaults}).
 *
 * @internal
 */
export const layerDefaults = new Map<Class<DeckLayer>, DefaultProps>([
  // VectorTileLayer and any other TileLayer subclass inherit this.
  [
    TileLayer,
    {
      refinementStrategy: 'no-overlap',
      // Suppress propagation of CARTO Maps API "no geometry rows" errors —
      // returning true from onError stops deck.gl's Layer.raiseError from
      // forwarding to the context/global error handler.
      onError: isEmptyGeometrySourceError,
    },
  ],
])

/**
 * Builds a layer config of `type` with the library's default props injected
 * beneath `layer.props`, ready to pass straight to `setLayer`. Precedence:
 * deck.gl defaults < library defaults < the caller's `props`.
 *
 * Defaults are matched up the class hierarchy: a subclass (e.g. `VectorTileLayer`)
 * inherits the defaults registered for an ancestor (e.g. `TileLayer`).
 *
 * Use this when **creating** a layer. For incremental updates use plain
 * `setLayer(id, { props })` — wrapping an update would re-assert the defaults
 * over the layer's live state. The defaults↔caller props merge is shallow;
 * nested default objects/arrays are shared with the registry, so don't mutate
 * them in place.
 *
 * @example
 *   setLayer(id, withLayerDefaults(VectorTileLayer, { name: 'Counties', props: { data } }))
 *   // → { type: VectorTileLayer, name: 'Counties', props: { refinementStrategy: 'no-overlap', data } }
 */
export function withLayerDefaults<T extends Class<DeckLayer>>(
  type: T,
  layer: Omit<Partial<Layer>, 'type'> = {},
): Layer {
  return {
    ...layer,
    type,
    props: { ...resolveLayerDefaults(type), ...layer.props },
  }
}

function resolveLayerDefaults(
  type: Class<DeckLayer>,
): DefaultProps | undefined {
  // Walk the prototype chain (each ES class' prototype is its parent class)
  // collecting every registered entry; stops at Function.prototype.
  const chain: DefaultProps[] = [] // [mostSpecific, ..., leastSpecific]
  for (
    let c: unknown = type;
    typeof c === 'function';
    c = Object.getPrototypeOf(c)
  ) {
    const d = layerDefaults.get(c as Class<DeckLayer>)
    if (d) chain.push(d)
  }
  if (!chain.length) return undefined
  // Shallow merge: least-specific ancestor as base, most-specific spread last
  // so a subclass overrides an ancestor on key conflicts.
  return chain.reduceRight((acc, d) => ({ ...acc, ...d }), {} as DefaultProps)
}
