import { Directives } from '../doc/directives.mjs';
import { Document } from '../doc/Document.mjs';
import { YAMLWarning, YAMLParseError } from '../errors.mjs';
import { isCollection, isPair } from '../nodes/identity.mjs';
import { composeDoc } from './compose-doc.mjs';
import { resolveEnd } from './resolve-end.mjs';

function getErrorPos(src) {
  if (typeof src === 'number') return [src, src + 1];
  if (Array.isArray(src)) return src.length === 2 ? src : [src[0], src[1]];
  const {
    offset,
    source
  } = src;
  return [offset, offset + (typeof source === 'string' ? source.length : 1)];
}
function parsePrelude(prelude) {
  let comment = '';
  let atComment = false;
  let afterEmptyLine = false;
  for (let i = 0; i < prelude.length; ++i) {
    const source = prelude[i];
    switch (source[0]) {
      case '#':
        comment += (comment === '' ? '' : afterEmptyLine ? '\n\n' : '\n') + (source.substring(1) || ' ');
        atComment = true;
        afterEmptyLine = false;
        break;
      case '%':
        if (prelude[i + 1]?.[0] !== '#') i += 1;
        atComment = false;
        break;
      default:
        // This may be wrong after doc-end, but in that case it doesn't matter
        if (!atComment) afterEmptyLine = true;
        atComment = false;
    }
  }
  return {
    comment,
    afterEmptyLine
  };
}
/**
 * Compose a stream of CST nodes into a stream of YAML Documents.
 *
 * ```ts
 * import { Composer, Parser } from 'yaml'
 *
 * const src: string = ...
 * const tokens = new Parser().parse(src)
 * const docs = new Composer().compose(tokens)
 * ```
 */
class Composer {
  constructor(options = {}) {
    this.doc = null;
    this.atDirectives = false;
    this.prelude = [];
    this.errors = [];
    this.warnings = [];
    this.onError = (source, code, message, warning) => {
      const pos = getErrorPos(source);
      if (warning) this.warnings.push(new YAMLWarning(pos, code, message));else this.errors.push(new YAMLParseError(pos, code, message));
    };
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
    this.directives = new Directives({
      version: options.version || '1.2'
    });
    this.options = options;
  }
  decorate(doc, afterDoc) {
    const {
      comment,
      afterEmptyLine
    } = parsePrelude(this.prelude);
    //console.log({ dc: doc.comment, prelude, comment })
    if (comment) {
      const dc = doc.contents;
      if (afterDoc) {
        doc.comment = doc.comment ? `${doc.comment}\n${comment}` : comment;
      } else if (afterEmptyLine || doc.directives.docStart || !dc) {
        doc.commentBefore = comment;
      } else if (isCollection(dc) && !dc.flow && dc.items.length > 0) {
        let it = dc.items[0];
        if (isPair(it)) it = it.key;
        const cb = it.commentBefore;
        it.commentBefore = cb ? `${comment}\n${cb}` : comment;
      } else {
        const cb = dc.commentBefore;
        dc.commentBefore = cb ? `${comment}\n${cb}` : comment;
      }
    }
    if (afterDoc) {
      Array.prototype.push.apply(doc.errors, this.errors);
      Array.prototype.push.apply(doc.warnings, this.warnings);
    } else {
      doc.errors = this.errors;
      doc.warnings = this.warnings;
    }
    this.prelude = [];
    this.errors = [];
    this.warnings = [];
  }
  /**
   * Current stream status information.
   *
   * Mostly useful at the end of input for an empty stream.
   */
  streamInfo() {
    return {
      comment: parsePrelude(this.prelude).comment,
      directives: this.directives,
      errors: this.errors,
      warnings: this.warnings
    };
  }
  /**
   * Compose tokens into documents.
   *
   * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
   * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
   */
  *compose(tokens, forceDoc = false, endOffset = -1) {
    for (const token of tokens) yield* this.next(token);
    yield* this.end(forceDoc, endOffset);
  }
  /** Advance the composer by one CST token. */
  *next(token) {
    switch (token.type) {
      case 'directive':
        this.directives.add(token.source, (offset, message, warning) => {
          const pos = getErrorPos(token);
          pos[0] += offset;
          this.onError(pos, 'BAD_DIRECTIVE', message, warning);
        });
        this.prelude.push(token.source);
        this.atDirectives = true;
        break;
      case 'document':
        {
          const doc = composeDoc(this.options, this.directives, token, this.onError);
          if (this.atDirectives && !doc.directives.docStart) this.onError(token, 'MISSING_CHAR', 'Missing directives-end/doc-start indicator line');
          this.decorate(doc, false);
          if (this.doc) yield this.doc;
          this.doc = doc;
          this.atDirectives = false;
          break;
        }
      case 'byte-order-mark':
      case 'space':
        break;
      case 'comment':
      case 'newline':
        this.prelude.push(token.source);
        break;
      case 'error':
        {
          const msg = token.source ? `${token.message}: ${JSON.stringify(token.source)}` : token.message;
          const error = new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg);
          if (this.atDirectives || !this.doc) this.errors.push(error);else this.doc.errors.push(error);
          break;
        }
      case 'doc-end':
        {
          if (!this.doc) {
            const msg = 'Unexpected doc-end without preceding document';
            this.errors.push(new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', msg));
            break;
          }
          this.doc.directives.docEnd = true;
          const end = resolveEnd(token.end, token.offset + token.source.length, this.doc.options.strict, this.onError);
          this.decorate(this.doc, true);
          if (end.comment) {
            const dc = this.doc.comment;
            this.doc.comment = dc ? `${dc}\n${end.comment}` : end.comment;
          }
          this.doc.range[2] = end.offset;
          break;
        }
      default:
        this.errors.push(new YAMLParseError(getErrorPos(token), 'UNEXPECTED_TOKEN', `Unsupported token ${token.type}`));
    }
  }
  /**
   * Call at end of input to yield any remaining document.
   *
   * @param forceDoc - If the stream contains no document, still emit a final document including any comments and directives that would be applied to a subsequent document.
   * @param endOffset - Should be set if `forceDoc` is also set, to set the document range end and to indicate errors correctly.
   */
  *end(forceDoc = false, endOffset = -1) {
    if (this.doc) {
      this.decorate(this.doc, true);
      yield this.doc;
      this.doc = null;
    } else if (forceDoc) {
      const opts = Object.assign({
        _directives: this.directives
      }, this.options);
      const doc = new Document(undefined, opts);
      if (this.atDirectives) this.onError(endOffset, 'MISSING_CHAR', 'Missing directives-end indicator line');
      doc.range = [0, endOffset, endOffset];
      this.decorate(doc, false);
      yield doc;
    }
  }
}

export { Composer };
