import {
  GenericIndexFields,
  GenericDocument,
  FieldTypeFromFieldPath,
} from "./data_model.js";

/**
 * A type that adds 1 to a number literal type (up to 14).
 *
 * This is necessary to step through the fields in an index.
 */
type PlusOne<N extends number> = [
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  9,
  10,
  11,
  12,
  13,
  14,
  15
][N];

/**
 * Builder to define an index range to query.
 *
 * An index range is a description of which documents Convex should consider
 * when running the query.
 *
 * An index range is always a chained list of:
 * 1. 0 or more equality expressions defined with `.eq`.
 * 2. [Optionally] A lower bound expression defined with `.gt` or `.gte`.
 * 3. [Optionally] An upper bound expression defined with `.lt` or `.lte`.
 *
 * **You must step through fields in index order.**
 *
 * Each equality expression must compare a different index field, starting from
 * the beginning and in order. The upper and lower bounds must follow the
 * equality expressions and compare the next field.
 *
 * For example, if there is an index of messages on
 * `["channel", "_creationTime"]`, a range searching for "messages in `channel`
 * created 1-2 minutes ago" would look like:
 * ```ts
 * q.eq("channel", channelId)
 *   .gt("_creationTime", Date.now() - 2 * 60000)
 *   .lt("_creationTime", Date.now() - 60000)
 * ```
 *
 * **The performance of your query is based on the specificity of the range.**
 *
 * This class is designed to only allow you to specify ranges that Convex can
 * efficiently use your index to find. For all other filtering use
 * {@link Query.filter}.
 *
 * To learn about indexes, see [Indexes](https://docs.convex.dev/using/indexes).
 * @public
 */
export interface IndexRangeBuilder<
  Document extends GenericDocument,
  IndexFields extends GenericIndexFields,
  FieldNum extends number = 0
> extends LowerBoundIndexRangeBuilder<Document, IndexFields[FieldNum]> {
  /**
   * Restrict this range to documents where `doc[fieldName] === value`.
   *
   * @param fieldName - The name of the field to compare. Must be the next field
   * in the index.
   * @param value - The value to compare against.
   */
  eq(
    fieldName: IndexFields[FieldNum],
    value: FieldTypeFromFieldPath<Document, IndexFields[FieldNum]>
  ): IndexRangeBuilder<Document, IndexFields, PlusOne<FieldNum>>;
}

/**
 * Builder to define the lower bound of an index range.
 *
 * See {@link IndexRangeBuilder}.
 *
 * @public
 */
export interface LowerBoundIndexRangeBuilder<
  Document extends GenericDocument,
  IndexFieldName extends string
> extends UpperBoundIndexRangeBuilder<Document, IndexFieldName> {
  /**
   * Restrict this range to documents where `doc[fieldName] > value`.
   *
   * @param fieldName - The name of the field to compare. Must be the next field
   * in the index.
   * @param value - The value to compare against.
   */
  gt(
    fieldName: IndexFieldName,
    value: FieldTypeFromFieldPath<Document, IndexFieldName>
  ): UpperBoundIndexRangeBuilder<Document, IndexFieldName>;
  /**
   * Restrict this range to documents where `doc[fieldName] >= value`.
   *
   * @param fieldName - The name of the field to compare. Must be the next field
   * in the index.
   * @param value - The value to compare against.
   */
  gte(
    fieldName: IndexFieldName,
    value: FieldTypeFromFieldPath<Document, IndexFieldName>
  ): UpperBoundIndexRangeBuilder<Document, IndexFieldName>;
}

/**
 * Builder to define the upper bound of an index range.
 *
 * See {@link IndexRangeBuilder}.
 *
 * @public
 */
export interface UpperBoundIndexRangeBuilder<
  Document extends GenericDocument,
  IndexFieldName extends string
> extends IndexRange {
  /**
   * Restrict this range to documents where `doc[fieldName] < value`.
   *
   * @param fieldName - The name of the field to compare. Must be the same index
   * field used in the lower bound (`.gt` or `.gte`) or the next field if no
   * lower bound was specified.
   * @param value - The value to compare against.
   */
  lt(
    fieldName: IndexFieldName,
    value: FieldTypeFromFieldPath<Document, IndexFieldName>
  ): IndexRange;

  /**
   * Restrict this range to documents where `doc[fieldName] <= value`.
   *
   * @param fieldName - The name of the field to compare. Must be the same index
   * field used in the lower bound (`.gt` or `.gte`) or the next field if no
   * lower bound was specified.
   * @param value - The value to compare against.
   */
  lte(
    fieldName: IndexFieldName,
    value: FieldTypeFromFieldPath<Document, IndexFieldName>
  ): IndexRange;
}

/**
 * An expression representing an index range created by
 * {@link IndexRangeBuilder}.
 * @public
 */
export abstract class IndexRange {
  // Property for nominal type support.
  private _isIndexRange: undefined;

  /**
   * @internal
   */
  constructor() {
    // only defining the constructor so we can mark it as internal and keep
    // it out of the docs.
  }
}
