import { isMosaicClient, MosaicClient } from './MosaicClient.js';
import type { ExprNode, ExprValue, ScaleOptions, ScaleDomain } from '@uwdata/mosaic-sql';
import { and, contains, isBetween, isInDistinct, isNotDistinct, listHasAll, listHasAny, literal, lower, or, prefix, regexp_matches, suffix } from '@uwdata/mosaic-sql';

/**
 * Selection clause metadata to guide possible query optimizations.
 * Sub-interfaces provide more information about the specifics of a
 * given selection based on the selection type.
 */
export interface ClauseMetadata {
  /**
   * The selection type, such as `'point'`, `'interval'`, or `'match'`.
   */
  type: string;
}

/**
 * Selection clause metadata indicating selection of one or more discrete
 * point values, typically based on equality or is distinctiveness checks.
 */
export interface PointMetadata extends ClauseMetadata {
  type: 'point';
}

/**
 * Selection clause metadata indicating text search matching.
 */
export interface MatchMetadata extends ClauseMetadata {
  type: 'match';
  /** The text search matching method used. */
  method?: 'contains' | 'prefix' | 'suffix' | 'regexp' | (string & {});
}

/** A binning method name. */
export type BinMethod = 'floor' | 'ceil' | 'round';

/**
 * Selection clause metadata for one or more selected intervals. This
 * metadata can be used to determine appropriate data-space binning
 * schemes that correspond to pixel-level bins in screen space.
 */
export interface IntervalMetadata extends ClauseMetadata {
  type: 'interval';
  /**
   * The interactive pixel size used by the generating component.
   * Values larger than one indicate intervals that "snap-to" values
   * greater than a single pixel. If unspecified, assumed to be `1`.
   */
  pixelSize?: number;
  /**
   * An array of one or more scale descriptors that describe the
   * mapping from data values to screen pixels.
   */
  scales?: ScaleOptions[];
  /**
   * A hint for the binning method to use when discretizing the
   * interval domain. If unspecified, the default is `'floor'`.
   */
  bin?: BinMethod
}

export type ClauseSource = object & { reset?: () => void; };

/**
 * A selection clause representing filtering criteria
 * to apply within a Mosaic Selection.
 */
export interface SelectionClause {
  /**
   * A unique identifier (according to object equality) for the source
   * component that generated this clause. In many cases, this is a
   * reference to the originating component itself.
   */
  source: ClauseSource;
  /**
   * A set of Mosaic clients associated with this clause that should not
   * be updated when this clause is applied in a cross-filtering context.
   */
  clients?: Set<MosaicClient>;
  /**
   * A selected value associated with this clause. For example, for a 1D
   * interval selection clause the value may be a [lo, hi] array.
   */
  value: unknown;
  /**
   * A predicate SQL expression suitable for use in a query WHERE clause.
   * The predicate should apply filtering criteria consistent with this
   * clause's *value* property.
   */
  predicate: ExprNode | null;
  /**
   * Optional clause metadata that varies based on the selection type.
   * The metadata can be used to optimize selection queries, for example
   * by creating materialized views of pre-aggregated data when applicable.
   */
  meta?: ClauseMetadata;
}

interface PointOptions {
  source: ClauseSource;
  clients?: Set<MosaicClient>;
}

/**
 * Generate a selection clause for a single selected point value.
 * @param field The table column or expression to select.
 * @param value The selected value.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients The Mosaic clients associated
 *  with this clause. These clients are not filtered by this clause in
 *  cross-filtering contexts.
 * @returns The generated selection clause.
 */
export function clausePoint(
  field: ExprValue,
  value: unknown, {
    source,
    clients = isMosaicClient(source) ? new Set([source]) : undefined
  }: PointOptions
): SelectionClause {
  const predicate: ExprNode | null = value !== undefined
    ? isInDistinct(field, [literal(value)])
    : null;
  return {
    meta: { type: 'point' },
    source,
    clients,
    value,
    predicate
  };
}

/**
 * Generate a selection clause for multiple selected point values.
 * @param fields The table columns or expressions to select.
 * @param value The selected values, as an array of
 *  arrays. Each subarray contains values for each *fields* entry.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients The Mosaic clients associated
 *  with this clause. These clients are not filtered by this clause in
 *  cross-filtering contexts.
 * @returns The generated selection clause.
 */
export function clausePoints(
  fields: ExprValue[],
  value: unknown[][] | null | undefined,
  {
    source,
    clients = isMosaicClient(source) ? new Set([source]) : undefined
  }: PointOptions
): SelectionClause {
  let predicate: ExprNode | null = null;
  if (value?.length) {
    const clauses = value.length && fields.length === 1
      ? [isInDistinct(fields[0], value.map(v => literal(v[0])))]
      : value.map(v => and(v.map((_, i) => isNotDistinct(fields[i], literal(_)))));
    predicate = value.length === 0 ? literal(false)
      : clauses.length > 1 ? or(clauses)
      : clauses[0];
  }
  return {
    meta: { type: 'point' },
    source,
    clients,
    value,
    predicate
  };
}

/** Interval selection clause options. */
interface IntervalOptions {
  source: ClauseSource;
  clients?: Set<MosaicClient>;
  bin?: BinMethod;
  pixelSize?: number;
}

/**
 * Generate a selection clause for a selected 1D interval.
 * @param field The table column or expression to select.
 * @param value The selected interval as a [lo, hi] array.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients The Mosaic clients associated
 *  with this clause. These clients are not filtered by this clause in
 *  cross-filtering contexts.
 * @param options.scale The scale mapping descriptor.
 * @param options.bin A binning method hint.
 * @param options.pixelSize The interactive pixel size.
 * @returns The generated selection clause.
 */
export function clauseInterval(
  field: ExprValue,
  value: ScaleDomain | null | undefined,
  {
    source,
    clients = isMosaicClient(source) ? new Set([source]) : undefined,
    bin,
    scale,
    pixelSize = 1
  }: IntervalOptions & { scale?: ScaleOptions }
): SelectionClause {
  const predicate = value != null ? isBetween(field, value) : null;
  const meta: IntervalMetadata = {
    type: 'interval',
    scales: scale && [scale],
    bin,
    pixelSize
  };
  return { meta, source, clients, value, predicate };
}

/**
 * Generate a selection clause for multiple selected intervals.
 * @param fields The table columns or expressions to select.
 * @param value The selected intervals, as an array of extents.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients The Mosaic clients associated
 *  with this clause. These clients are not filtered by this clause in
 *  cross-filtering contexts.
 * @param options.scales The scale mapping descriptors,
 *  in an order matching the given *fields* and *value* extents.
 * @param options.bin A binning method hint.
 * @param options.pixelSize The interactive pixel size.
 * @returns The generated selection clause.
 */
export function clauseIntervals(
  fields: ExprValue[],
  value: ScaleDomain[] | null | undefined,
  {
    source,
    clients = isMosaicClient(source) ? new Set([source]) : undefined,
    bin,
    scales = [],
    pixelSize = 1
  }: IntervalOptions & { scales?: ScaleOptions[] }
): SelectionClause {
  const predicate = value != null
    ? and(fields.map((f, i) => isBetween(f, value[i])))
    : null;
  const meta: IntervalMetadata = {
    type: 'interval',
    scales,
    bin,
    pixelSize
  };
  return { meta, source, clients, value, predicate };
}

const identity = (x: string | ExprNode) => x;

const MATCH_METHODS = { contains, prefix, suffix, regexp: regexp_matches };

/** Text search matching methods. */
export type MatchMethod = keyof typeof MATCH_METHODS;

/** Text matching selection clause options. */
export interface MatchOptions {
  source: ClauseSource;
  clients?: Set<MosaicClient>;
  method?: MatchMethod;
  caseSensitive?: boolean;
}

/**
 * Generate a selection clause for text search matching over a single column.
 * @param field The table column or expression to match.
 * @param value The selected text search query string.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients Mosaic clients associated with this clause.
 *  These clients are not filtered by this clause in cross-filtering contexts.
 * @param options.method The text matching method to use, default `'contains'`.
 * @param options.caseSensitive Flag for case sensitive matching, default `false`.
 * @returns The generated selection clause.
 */
export function clauseMatch(
  field: string | ExprNode,
  value: string | null | undefined,
  {
    source,
    clients = undefined,
    method = 'contains',
    caseSensitive = false
  }: MatchOptions
): SelectionClause {
  const fn = MATCH_METHODS[method as keyof typeof MATCH_METHODS];
  const transform = caseSensitive ? identity: lower;
  const predicate = value ? fn(transform(field), transform(literal(value))) : null;
  const meta: MatchMetadata = { type: 'match', method };
  return { meta, source, clients, value, predicate };
}

/**
 * Generate a selection clause for text search matching over multiple columns.
 * A match will succeed if any field successfully matches.
 * @param fields The table columns or expressions to match.
 * @param value The selected text search query string.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients Mosaic clients associated with this clause.
 *  These clients are not filtered by this clause in cross-filtering contexts.
 * @param options.method The text matching method to use, default `'contains'`.
 * @param options.caseSensitive Flag for case sensitive matching, default `false`.
 * @returns The generated selection clause.
 */
export function clauseMatchAny(
  fields: (string | ExprNode)[],
  value: string | null,
  {
    source,
    clients = undefined,
    method = 'contains',
    caseSensitive = false
  }: MatchOptions
): SelectionClause {
  value = value || null;
  const fn = MATCH_METHODS[method];
  const transform = caseSensitive ? identity : lower;
  const query = transform(literal(value));
  const predicate = value
    ? or(fields.flatMap(field => value ? fn(transform(field), query) : []))
    : null;
  const meta: MatchMetadata = { type: 'match', method };
  return { meta, source, clients, value, predicate };
}

/**
 * Generate a selection clause for a single selected point value in a list.
 * @param field The table column or expression to select, which must be a list.
 * @param value The selected value.
 * @param options Additional clause properties.
 * @param options.source The source component generating this clause.
 * @param options.clients The Mosaic clients associated
 *  with this clause. These clients are not filtered by this clause in
 *  cross-filtering contexts.
 * @returns The generated selection clause.
 */
export function clauseList(
  field: ExprValue,
  value: unknown,
  {
    source,
    clients = isMosaicClient(source) ? new Set([source]) : undefined,
    listMatch = 'any'
  }: {
    source: ClauseSource;
    clients?: Set<MosaicClient>;
    listMatch?: 'any' | 'all';
  }
): SelectionClause {
  const listFn = listMatch === 'all' ? listHasAll : listHasAny;
  const predicate = value !== undefined ? listFn(field, literal(value)) : null;
  return { source, clients, value, predicate };
}
