import { Alias } from '../nodes/Alias.mjs';
import { isEmptyPath, collectionFromPath } from '../nodes/Collection.mjs';
import { NODE_TYPE, DOC, isNode, isCollection, isScalar } from '../nodes/identity.mjs';
import { Pair } from '../nodes/Pair.mjs';
import { toJS } from '../nodes/toJS.mjs';
import { Schema } from '../schema/Schema.mjs';
import { stringifyDocument } from '../stringify/stringifyDocument.mjs';
import { anchorNames, findNewAnchor, createNodeAnchors } from './anchors.mjs';
import { applyReviver } from './applyReviver.mjs';
import { createNode } from './createNode.mjs';
import { Directives } from './directives.mjs';

class Document {
  constructor(value, replacer, options) {
    /** A comment before this Document */
    this.commentBefore = null;
    /** A comment immediately after this Document */
    this.comment = null;
    /** Errors encountered during parsing. */
    this.errors = [];
    /** Warnings encountered during parsing. */
    this.warnings = [];
    Object.defineProperty(this, NODE_TYPE, {
      value: DOC
    });
    let _replacer = null;
    if (typeof replacer === 'function' || Array.isArray(replacer)) {
      _replacer = replacer;
    } else if (options === undefined && replacer) {
      options = replacer;
      replacer = undefined;
    }
    const opt = Object.assign({
      intAsBigInt: false,
      keepSourceTokens: false,
      logLevel: 'warn',
      prettyErrors: true,
      strict: true,
      stringKeys: false,
      uniqueKeys: true,
      version: '1.2'
    }, options);
    this.options = opt;
    let {
      version
    } = opt;
    if (options?._directives) {
      this.directives = options._directives.atDocument();
      if (this.directives.yaml.explicit) version = this.directives.yaml.version;
    } else this.directives = new Directives({
      version
    });
    this.setSchema(version, options);
    // @ts-expect-error We can't really know that this matches Contents.
    this.contents = value === undefined ? null : this.createNode(value, _replacer, options);
  }
  /**
   * Create a deep copy of this Document and its contents.
   *
   * Custom Node values that inherit from `Object` still refer to their original instances.
   */
  clone() {
    const copy = Object.create(Document.prototype, {
      [NODE_TYPE]: {
        value: DOC
      }
    });
    copy.commentBefore = this.commentBefore;
    copy.comment = this.comment;
    copy.errors = this.errors.slice();
    copy.warnings = this.warnings.slice();
    copy.options = Object.assign({}, this.options);
    if (this.directives) copy.directives = this.directives.clone();
    copy.schema = this.schema.clone();
    // @ts-expect-error We can't really know that this matches Contents.
    copy.contents = isNode(this.contents) ? this.contents.clone(copy.schema) : this.contents;
    if (this.range) copy.range = this.range.slice();
    return copy;
  }
  /** Adds a value to the document. */
  add(value) {
    if (assertCollection(this.contents)) this.contents.add(value);
  }
  /** Adds a value to the document. */
  addIn(path, value) {
    if (assertCollection(this.contents)) this.contents.addIn(path, value);
  }
  /**
   * Create a new `Alias` node, ensuring that the target `node` has the required anchor.
   *
   * If `node` already has an anchor, `name` is ignored.
   * Otherwise, the `node.anchor` value will be set to `name`,
   * or if an anchor with that name is already present in the document,
   * `name` will be used as a prefix for a new unique anchor.
   * If `name` is undefined, the generated anchor will use 'a' as a prefix.
   */
  createAlias(node, name) {
    if (!node.anchor) {
      const prev = anchorNames(this);
      node.anchor =
      // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
      !name || prev.has(name) ? findNewAnchor(name || 'a', prev) : name;
    }
    return new Alias(node.anchor);
  }
  createNode(value, replacer, options) {
    let _replacer = undefined;
    if (typeof replacer === 'function') {
      value = replacer.call({
        '': value
      }, '', value);
      _replacer = replacer;
    } else if (Array.isArray(replacer)) {
      const keyToStr = v => typeof v === 'number' || v instanceof String || v instanceof Number;
      const asStr = replacer.filter(keyToStr).map(String);
      if (asStr.length > 0) replacer = replacer.concat(asStr);
      _replacer = replacer;
    } else if (options === undefined && replacer) {
      options = replacer;
      replacer = undefined;
    }
    const {
      aliasDuplicateObjects,
      anchorPrefix,
      flow,
      keepUndefined,
      onTagObj,
      tag
    } = options ?? {};
    const {
      onAnchor,
      setAnchors,
      sourceObjects
    } = createNodeAnchors(this,
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
    anchorPrefix || 'a');
    const ctx = {
      aliasDuplicateObjects: aliasDuplicateObjects ?? true,
      keepUndefined: keepUndefined ?? false,
      onAnchor,
      onTagObj,
      replacer: _replacer,
      schema: this.schema,
      sourceObjects
    };
    const node = createNode(value, tag, ctx);
    if (flow && isCollection(node)) node.flow = true;
    setAnchors();
    return node;
  }
  /**
   * Convert a key and a value into a `Pair` using the current schema,
   * recursively wrapping all values as `Scalar` or `Collection` nodes.
   */
  createPair(key, value, options = {}) {
    const k = this.createNode(key, null, options);
    const v = this.createNode(value, null, options);
    return new Pair(k, v);
  }
  /**
   * Removes a value from the document.
   * @returns `true` if the item was found and removed.
   */
  delete(key) {
    return assertCollection(this.contents) ? this.contents.delete(key) : false;
  }
  /**
   * Removes a value from the document.
   * @returns `true` if the item was found and removed.
   */
  deleteIn(path) {
    if (isEmptyPath(path)) {
      if (this.contents == null) return false;
      // @ts-expect-error Presumed impossible if Strict extends false
      this.contents = null;
      return true;
    }
    return assertCollection(this.contents) ? this.contents.deleteIn(path) : false;
  }
  /**
   * 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, keepScalar) {
    return isCollection(this.contents) ? this.contents.get(key, keepScalar) : undefined;
  }
  /**
   * Returns item at `path`, 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).
   */
  getIn(path, keepScalar) {
    if (isEmptyPath(path)) return !keepScalar && isScalar(this.contents) ? this.contents.value : this.contents;
    return isCollection(this.contents) ? this.contents.getIn(path, keepScalar) : undefined;
  }
  /**
   * Checks if the document includes a value with the key `key`.
   */
  has(key) {
    return isCollection(this.contents) ? this.contents.has(key) : false;
  }
  /**
   * Checks if the document includes a value at `path`.
   */
  hasIn(path) {
    if (isEmptyPath(path)) return this.contents !== undefined;
    return isCollection(this.contents) ? this.contents.hasIn(path) : false;
  }
  /**
   * Sets a value in this document. For `!!set`, `value` needs to be a
   * boolean to add/remove the item from the set.
   */
  set(key, value) {
    if (this.contents == null) {
      // @ts-expect-error We can't really know that this matches Contents.
      this.contents = collectionFromPath(this.schema, [key], value);
    } else if (assertCollection(this.contents)) {
      this.contents.set(key, value);
    }
  }
  /**
   * Sets a value in this document. For `!!set`, `value` needs to be a
   * boolean to add/remove the item from the set.
   */
  setIn(path, value) {
    if (isEmptyPath(path)) {
      // @ts-expect-error We can't really know that this matches Contents.
      this.contents = value;
    } else if (this.contents == null) {
      // @ts-expect-error We can't really know that this matches Contents.
      this.contents = collectionFromPath(this.schema, Array.from(path), value);
    } else if (assertCollection(this.contents)) {
      this.contents.setIn(path, value);
    }
  }
  /**
   * Change the YAML version and schema used by the document.
   * A `null` version disables support for directives, explicit tags, anchors, and aliases.
   * It also requires the `schema` option to be given as a `Schema` instance value.
   *
   * Overrides all previously set schema options.
   */
  setSchema(version, options = {}) {
    if (typeof version === 'number') version = String(version);
    let opt;
    switch (version) {
      case '1.1':
        if (this.directives) this.directives.yaml.version = '1.1';else this.directives = new Directives({
          version: '1.1'
        });
        opt = {
          resolveKnownTags: false,
          schema: 'yaml-1.1'
        };
        break;
      case '1.2':
      case 'next':
        if (this.directives) this.directives.yaml.version = version;else this.directives = new Directives({
          version
        });
        opt = {
          resolveKnownTags: true,
          schema: 'core'
        };
        break;
      case null:
        if (this.directives) delete this.directives;
        opt = null;
        break;
      default:
        {
          const sv = JSON.stringify(version);
          throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`);
        }
    }
    // Not using `instanceof Schema` to allow for duck typing
    if (options.schema instanceof Object) this.schema = options.schema;else if (opt) this.schema = new Schema(Object.assign(opt, options));else throw new Error(`With a null YAML version, the { schema: Schema } option is required`);
  }
  // json & jsonArg are only used from toJSON()
  toJS({
    json,
    jsonArg,
    mapAsMap,
    maxAliasCount,
    onAnchor,
    reviver
  } = {}) {
    const ctx = {
      anchors: new Map(),
      doc: this,
      keep: !json,
      mapAsMap: mapAsMap === true,
      mapKeyWarned: false,
      maxAliasCount: typeof maxAliasCount === 'number' ? maxAliasCount : 100
    };
    const res = toJS(this.contents, jsonArg ?? '', ctx);
    if (typeof onAnchor === 'function') for (const {
      count,
      res
    } of ctx.anchors.values()) onAnchor(res, count);
    return typeof reviver === 'function' ? applyReviver(reviver, {
      '': res
    }, '', res) : res;
  }
  /**
   * A JSON representation of the document `contents`.
   *
   * @param jsonArg Used by `JSON.stringify` to indicate the array index or
   *   property name.
   */
  toJSON(jsonArg, onAnchor) {
    return this.toJS({
      json: true,
      jsonArg,
      mapAsMap: false,
      onAnchor
    });
  }
  /** A YAML representation of the document. */
  toString(options = {}) {
    if (this.errors.length > 0) throw new Error('Document with errors cannot be stringified');
    if ('indent' in options && (!Number.isInteger(options.indent) || Number(options.indent) <= 0)) {
      const s = JSON.stringify(options.indent);
      throw new Error(`"indent" option must be a positive integer, not ${s}`);
    }
    return stringifyDocument(this, options);
  }
}
function assertCollection(contents) {
  if (isCollection(contents)) return true;
  throw new Error('Expected a YAML collection as document contents');
}

export { Document };
