// deno-lint-ignore-file no-explicit-any
/**
 * MapLibre expression builders — named helpers that replace raw expression
 * arrays, making layer definitions more readable.
 *
 * @module
 */

// ─── Property access ────────────────────────────────────────────────────────

/** Get a feature property value: `['get', prop]` */
export const get = (prop: string): any[] => ['get', prop]

/** Return the first non-null value: `['coalesce', ...args]` */
export const coalesce = (...args: any[]): any[] => ['coalesce', ...args]

/** Wrap a value as a literal: `['literal', value]` */
export const literal = (value: unknown): any[] => ['literal', value]

// ─── Interpolation ──────────────────────────────────────────────────────────

type ZoomStop = [number, any]

/** Linear interpolation over zoom, with `[zoom, value]` stop pairs */
export const linear = (
  ...zoomStops: ZoomStop[]
): any[] => ['interpolate', ['linear'], ['zoom'], ...zoomStops.flat()]

/** Exponential interpolation over zoom, with `[zoom, value]` stop pairs */
export const exp = (
  base: number,
  ...zoomStops: ZoomStop[]
): any[] => [
  'interpolate',
  ['exponential', base],
  ['zoom'],
  ...zoomStops.flat(),
]

// ─── Conditional ─────────────────────────────────────────────────────────────

/**
 * Match a feature property against value/result pairs.
 *
 * `match('class', { minor: '#efefef', service: '#ddd' }, '#fff')`
 * produces `['match', ['get', 'class'], 'minor', '#efefef', 'service', '#ddd', '#fff']`
 */
export function match(
  property: string,
  cases: Record<string, unknown>,
  fallback: unknown,
): any[] {
  return ['match', ['get', property], ...Object.entries(cases).flat(), fallback]
}

/** Conditional expression: `['case', ...args]` */
export const cond = (...args: any[]): any[] => ['case', ...args]

// ─── mz fade ─────────────────────────────────────────────────────────────────

const MZ: any[] = ['coalesce', ['get', 'mz'], 0]

/**
 * Zoom-stop pair for mz-based feature fade-in.
 * Returns a `[zoom, value]` tuple — pass directly to `linear()`/`exp()`.
 *
 * Features with mz <= zFinish show at target opacity; others are hidden.
 * @param zFinish - mz threshold (opacity reaches target)
 * @param zStart  - zoom level for this stop
 * @param opacity - target opacity (default 1)
 */
export const mzFade = (
  zFinish: number,
  zStart: number,
  opacity: any = 1,
): [number, any[]] => [zStart, ['case', ['<=', MZ, zFinish], opacity, 0]]

// ─── Legacy zoom stops ──────────────────────────────────────────────────────

type Stop = [number, unknown]

/** Legacy zoom function with optional exponential base */
export function stops(first: number | Stop, ...rest: Stop[]) {
  if (typeof first === 'number') {
    return { base: first, stops: rest }
  }
  return { stops: [first, ...rest] }
}

// ─── Filters ─────────────────────────────────────────────────────────────────

/** Geometry type: LineString */
export const isLine: any[] = ['==', '$type', 'LineString']
/** Geometry type: Polygon */
export const isPoly: any[] = ['==', '$type', 'Polygon']
/** Geometry type: Point */
export const isPoint: any[] = ['==', '$type', 'Point']

/** Combine filters: `['all', ...conditions]` */
export const all = (...conditions: any[]): any[] => ['all', ...conditions]
/** Combine expressions with OR: `['any', ...conditions]` */
export const any = (...conditions: any[]): any[] => ['any', ...conditions]

/** Equality filter: `['==', prop, value]` */
export const eq = (prop: string, value: unknown): any[] => ['==', prop, value]
/** Inequality filter: `['!=', prop, value]` */
export const neq = (prop: string, value: unknown): any[] => ['!=', prop, value]
/** Less-than-or-equal filter: `['<=', prop, value]` */
export const lte = (prop: string, value: unknown): any[] => ['<=', prop, value]
/** Greater-than-or-equal filter: `['>=', prop, value]` */
export const gte = (prop: string, value: unknown): any[] => ['>=', prop, value]

/** Membership filter: `['in', prop, ...values]` */
export const isIn = (
  prop: string,
  ...values: unknown[]
): any[] => ['in', prop, ...values]
/** Negated membership filter: `['!in', prop, ...values]` */
export const notIn = (
  prop: string,
  ...values: unknown[]
): any[] => ['!in', prop, ...values]

/** Property existence filter: `['has', prop]` */
export const has = (prop: string): any[] => ['has', prop]

/** Expression-style membership: `['in', ['get', prop], ['literal', values]]` */
export const includes = (
  prop: string,
  values: unknown[],
): any[] => ['in', ['get', prop], ['literal', values]]

/**
 * Substring containment against a (possibly semicolon-joined) property value:
 * `['in', needle, ['get', prop]]`. MapLibre's `in` expression checks substring
 * membership when the haystack is a string, which is what multi-value OSM
 * tags like `network=Métro de Paris;RATP` end up as after tilemaker writes
 * them through unsplit.
 */
export const contains = (needle: string, prop: string): any[] => [
  'in',
  needle,
  ['get', prop],
]
