import { Document, scalarOptions } from './index'
import { CST } from './parse-cst'
import { Type } from './util'

export const binaryOptions: scalarOptions.Binary
export const boolOptions: scalarOptions.Bool
export const intOptions: scalarOptions.Int
export const nullOptions: scalarOptions.Null
export const strOptions: scalarOptions.Str

export class Schema {
  /** Default: `'tag:yaml.org,2002:'` */
  static defaultPrefix: string
  static defaultTags: {
    /** Default: `'tag:yaml.org,2002:map'` */
    MAP: string
    /** Default: `'tag:yaml.org,2002:seq'` */
    SEQ: string
    /** Default: `'tag:yaml.org,2002:str'` */
    STR: string
  }
  constructor(options: Schema.Options)
  /**
   * Convert any value into a `Node` using this schema, recursively turning
   * objects into collectsions.
   *
   * @param wrapScalars If `true`, also wraps plain values in `Scalar` objects;
   *   if undefined or `false` and `value` is not an object, it will be returned
   *   directly.
   * @param tag Use to specify the collection type, e.g. `"!!omap"`. Note that
   *   this requires the corresponding tag to be available in this schema.
   */
  createNode(
    value: any,
    wrapScalars?: boolean,
    tag?: string,
    ctx?: Schema.CreateNodeContext
  ): AST.Node
  merge: boolean
  name: Schema.Name
  sortMapEntries: ((a: Pair, b: Pair) => number) | null
  tags: Schema.Tag[]
}

export namespace Schema {
  type Name = 'core' | 'failsafe' | 'json' | 'yaml-1.1'

  interface Options {
    /**
     * Array of additional tags to include in the schema, or a function that may
     * modify the schema's base tag array.
     */
    customTags?: (TagId | Tag)[] | ((tags: Tag[]) => Tag[])
    /**
     * Enable support for `<<` merge keys.
     *
     * Default: `false` for YAML 1.2, `true` for earlier versions
     */
    merge?: boolean
    /**
     * The base schema to use.
     *
     * Default: `"core"` for YAML 1.2, `"yaml-1.1"` for earlier versions
     */
    schema?: Name
    /**
     * When stringifying, sort map entries. If `true`, sort by comparing key values with `<`.
     *
     * Default: `false`
     */
    sortMapEntries?: boolean | ((a: Pair, b: Pair) => number)
    /**
     * @deprecated Use `customTags` instead.
     */
    tags?: Options['customTags']
  }

  interface CreateNodeContext {
    wrapScalars?: boolean
    [key: string]: any
  }

  interface StringifyContext {
    forceBlockIndent?: boolean
    implicitKey?: boolean
    indent?: string
    indentAtStart?: number
    inFlow?: boolean
    [key: string]: any
  }

  type TagId =
    | 'binary'
    | 'bool'
    | 'float'
    | 'floatExp'
    | 'floatNaN'
    | 'floatTime'
    | 'int'
    | 'intHex'
    | 'intOct'
    | 'intTime'
    | 'null'
    | 'omap'
    | 'pairs'
    | 'set'
    | 'timestamp'

  type Tag = CustomTag | DefaultTag

  interface BaseTag {
    /**
     * An optional factory function, used e.g. by collections when wrapping JS objects as AST nodes.
     */
    createNode?: (
      schema: Schema,
      value: any,
      ctx: Schema.CreateNodeContext
    ) => YAMLMap | YAMLSeq | Scalar
    /**
     * If a tag has multiple forms that should be parsed and/or stringified differently, use `format` to identify them.
     */
    format?: string
    /**
     * Used by `YAML.createNode` to detect your data type, e.g. using `typeof` or
     * `instanceof`.
     */
    identify(value: any): boolean
    /**
     * The `Node` child class that implements this tag. Required for collections and tags that have overlapping JS representations.
     */
    nodeClass?: new () => any
    /**
     * Used by some tags to configure their stringification, where applicable.
     */
    options?: object
    /**
     * Optional function stringifying the AST node in the current context. If your
     * data includes a suitable `.toString()` method, you can probably leave this
     * undefined and use the default stringifier.
     *
     * @param item The node being stringified.
     * @param ctx Contains the stringifying context variables.
     * @param onComment Callback to signal that the stringifier includes the
     *   item's comment in its output.
     * @param onChompKeep Callback to signal that the output uses a block scalar
     *   type with the `+` chomping indicator.
     */
    stringify?: (
      item: AST.Node,
      ctx: Schema.StringifyContext,
      onComment?: () => void,
      onChompKeep?: () => void
    ) => string
    /**
     * The identifier for your data type, with which its stringified form will be
     * prefixed. Should either be a !-prefixed local `!tag`, or a fully qualified
     * `tag:domain,date:foo`.
     */
    tag: string
  }

  interface CustomTag extends BaseTag {
    /**
     * A JavaScript class that should be matched to this tag, e.g. `Date` for `!!timestamp`.
     * @deprecated Use `Tag.identify` instead
     */
    class?: new () => any
    /**
     * Turns a CST node into an AST node. If returning a non-`Node` value, the
     * output will be wrapped as a `Scalar`.
     */
    resolve(doc: Document, cstNode: CST.Node): AST.Node | any
  }

  interface DefaultTag extends BaseTag {
    /**
     * If `true`, together with `test` allows for values to be stringified without
     * an explicit tag. For most cases, it's unlikely that you'll actually want to
     * use this, even if you first think you do.
     */
    default: true
    /**
     * Alternative form used by default tags; called with `test` match results.
     */
    resolve(...match: string[]): AST.Node | any
    /**
     * Together with `default` allows for values to be stringified without an
     * explicit tag and detected using a regular expression. For most cases, it's
     * unlikely that you'll actually want to use these, even if you first think
     * you do.
     */
    test: RegExp
  }
}

export class Scalar extends AST.Node {
  constructor(value: any)
  toJSON(arg?: any, ctx?: AST.NodeToJsonContext): any
  type?: Scalar.Type
  /**
   * By default (undefined), numbers use decimal notation.
   * The YAML 1.2 core schema only supports 'HEX' and 'OCT'.
   */
  format?: 'BIN' | 'HEX' | 'OCT' | 'TIME'
  value: any
}
export namespace Scalar {
  type Type =
    | Type.BLOCK_FOLDED
    | Type.BLOCK_LITERAL
    | Type.PLAIN
    | Type.QUOTE_DOUBLE
    | Type.QUOTE_SINGLE
}

export class Pair extends AST.Node {
  constructor(key: any, value?: any)
  toJSON(arg?: any, ctx?: AST.NodeToJsonContext): object | Map<any, any>
  type: Pair.Type.PAIR | Pair.Type.MERGE_PAIR
  /** Always Node or null when parsed, but can be set to anything. */
  key: any
  /** Always Node or null when parsed, but can be set to anything. */
  value: any
  cstNode?: never // no corresponding cstNode
}
export namespace Pair {
  enum Type {
    PAIR = 'PAIR',
    MERGE_PAIR = 'MERGE_PAIR'
  }
}

export class YAMLMap extends AST.Collection {
  type?: Type.FLOW_MAP | Type.MAP
  items: Array<Pair | AST.Merge>
  hasAllNullValues(): boolean
  toJSON(arg?: any, ctx?: AST.NodeToJsonContext): object | Map<any, any>
}

export class YAMLSeq extends AST.Collection {
  type?: Type.FLOW_SEQ | Type.SEQ
  delete(key: number | string | Scalar): boolean
  get(key: number | string | Scalar, keepScalar?: boolean): any
  has(key: number | string | Scalar): boolean
  set(key: number | string | Scalar, value: any): void
  hasAllNullValues(): boolean
  toJSON(arg?: any, ctx?: AST.NodeToJsonContext): any[]
}

export namespace AST {
  type AstNode = ScalarNode | CollectionNode | Alias

  type ScalarNode =
    | BlockFolded
    | BlockLiteral
    | PlainValue
    | QuoteDouble
    | QuoteSingle

  type CollectionNode = FlowMap | BlockMap | FlowSeq | BlockSeq

  class Node {
    /** A comment on or immediately after this */
    comment?: string | null
    /** A comment before this */
    commentBefore?: string | null
    /** Only available when `keepCstNodes` is set to `true` */
    cstNode?: CST.Node
    /**
     * The [start, end] range of characters of the source parsed
     * into this node (undefined for pairs or if not parsed)
     */
    range?: [number, number] | null
    /** A blank line before this node and its commentBefore */
    spaceBefore?: boolean
    /** A fully qualified tag, if required */
    tag?: string
    /** A plain JS representation of this node */
    toJSON(arg?: any): any
    /** The type of this node */
    type?: Type | Pair.Type
  }

  class Collection extends Node {
    type?: Type.MAP | Type.FLOW_MAP | Type.SEQ | Type.FLOW_SEQ | Type.DOCUMENT
    items: any[]
    schema?: Schema

    /**
     * Adds a value to the collection. For `!!map` and `!!omap` the value must
     * be a Pair instance or a `{ key, value }` object, which may not have a key
     * that already exists in the map.
     */
    add(value: any): void
    addIn(path: Iterable<any>, value: any): void
    /**
     * Removes a value from the collection.
     * @returns `true` if the item was found and removed.
     */
    delete(key: any): boolean
    deleteIn(path: Iterable<any>): boolean
    /**
     * Returns item at `key`, or `undefined` if not found. By default unwraps
     * scalar values from their surrounding node; to disable set `keepScalar` to
     * `true` (collections are always returned intact).
     */
    get(key: any, keepScalar?: boolean): any
    getIn(path: Iterable<any>, keepScalar?: boolean): any
    /**
     * Checks if the collection includes a value with the key `key`.
     */
    has(key: any): boolean
    hasIn(path: Iterable<any>): boolean
    /**
     * Sets a value in this collection. For `!!set`, `value` needs to be a
     * boolean to add/remove the item from the set.
     */
    set(key: any, value: any): void
    setIn(path: Iterable<any>, value: any): void
  }

  interface NodeToJsonContext {
    anchors?: any[]
    doc: Document
    keep?: boolean
    mapAsMap?: boolean
    maxAliasCount?: number
    onCreate?: (node: Node) => void
    [key: string]: any
  }

  interface BlockFolded extends Scalar {
    type: Type.BLOCK_FOLDED
    cstNode?: CST.BlockFolded
  }

  interface BlockLiteral extends Scalar {
    type: Type.BLOCK_LITERAL
    cstNode?: CST.BlockLiteral
  }

  interface PlainValue extends Scalar {
    type: Type.PLAIN
    cstNode?: CST.PlainValue
  }

  interface QuoteDouble extends Scalar {
    type: Type.QUOTE_DOUBLE
    cstNode?: CST.QuoteDouble
  }

  interface QuoteSingle extends Scalar {
    type: Type.QUOTE_SINGLE
    cstNode?: CST.QuoteSingle
  }

  interface Alias extends Node {
    type: Type.ALIAS
    source: AstNode
    cstNode?: CST.Alias
  }

  interface Merge extends Pair {
    type: Pair.Type.MERGE_PAIR
    /** Always Scalar('<<'), defined by the type specification */
    key: PlainValue
    /** Always YAMLSeq<Alias(Map)>, stringified as *A if length = 1 */
    value: YAMLSeq
  }

  interface FlowMap extends YAMLMap {
    type: Type.FLOW_MAP
    cstNode?: CST.FlowMap
  }

  interface BlockMap extends YAMLMap {
    type: Type.MAP
    cstNode?: CST.Map
  }

  interface FlowSeq extends YAMLSeq {
    type: Type.FLOW_SEQ
    items: Array<AstNode | Pair>
    cstNode?: CST.FlowSeq
  }

  interface BlockSeq extends YAMLSeq {
    type: Type.SEQ
    items: Array<AstNode | null>
    cstNode?: CST.Seq
  }
}
