import { isNode } from '../nodes/identity.mjs';
import { visit } from '../visit.mjs';

const escapeChars = {
  '!': '%21',
  ',': '%2C',
  '[': '%5B',
  ']': '%5D',
  '{': '%7B',
  '}': '%7D'
};
const escapeTagName = tn => tn.replace(/[!,[\]{}]/g, ch => escapeChars[ch]);
class Directives {
  constructor(yaml, tags) {
    /**
     * The directives-end/doc-start marker `---`. If `null`, a marker may still be
     * included in the document's stringified representation.
     */
    this.docStart = null;
    /** The doc-end marker `...`.  */
    this.docEnd = false;
    this.yaml = Object.assign({}, Directives.defaultYaml, yaml);
    this.tags = Object.assign({}, Directives.defaultTags, tags);
  }
  clone() {
    const copy = new Directives(this.yaml, this.tags);
    copy.docStart = this.docStart;
    return copy;
  }
  /**
   * During parsing, get a Directives instance for the current document and
   * update the stream state according to the current version's spec.
   */
  atDocument() {
    const res = new Directives(this.yaml, this.tags);
    switch (this.yaml.version) {
      case '1.1':
        this.atNextDocument = true;
        break;
      case '1.2':
        this.atNextDocument = false;
        this.yaml = {
          explicit: Directives.defaultYaml.explicit,
          version: '1.2'
        };
        this.tags = Object.assign({}, Directives.defaultTags);
        break;
    }
    return res;
  }
  /**
   * @param onError - May be called even if the action was successful
   * @returns `true` on success
   */
  add(line, onError) {
    if (this.atNextDocument) {
      this.yaml = {
        explicit: Directives.defaultYaml.explicit,
        version: '1.1'
      };
      this.tags = Object.assign({}, Directives.defaultTags);
      this.atNextDocument = false;
    }
    const parts = line.trim().split(/[ \t]+/);
    const name = parts.shift();
    switch (name) {
      case '%TAG':
        {
          if (parts.length !== 2) {
            onError(0, '%TAG directive should contain exactly two parts');
            if (parts.length < 2) return false;
          }
          const [handle, prefix] = parts;
          this.tags[handle] = prefix;
          return true;
        }
      case '%YAML':
        {
          this.yaml.explicit = true;
          if (parts.length !== 1) {
            onError(0, '%YAML directive should contain exactly one part');
            return false;
          }
          const [version] = parts;
          if (version === '1.1' || version === '1.2') {
            this.yaml.version = version;
            return true;
          } else {
            const isValid = /^\d+\.\d+$/.test(version);
            onError(6, `Unsupported YAML version ${version}`, isValid);
            return false;
          }
        }
      default:
        onError(0, `Unknown directive ${name}`, true);
        return false;
    }
  }
  /**
   * Resolves a tag, matching handles to those defined in %TAG directives.
   *
   * @returns Resolved tag, which may also be the non-specific tag `'!'` or a
   *   `'!local'` tag, or `null` if unresolvable.
   */
  tagName(source, onError) {
    if (source === '!') return '!'; // non-specific tag
    if (source[0] !== '!') {
      onError(`Not a valid tag: ${source}`);
      return null;
    }
    if (source[1] === '<') {
      const verbatim = source.slice(2, -1);
      if (verbatim === '!' || verbatim === '!!') {
        onError(`Verbatim tags aren't resolved, so ${source} is invalid.`);
        return null;
      }
      if (source[source.length - 1] !== '>') onError('Verbatim tags must end with a >');
      return verbatim;
    }
    const [, handle, suffix] = source.match(/^(.*!)([^!]*)$/s);
    if (!suffix) onError(`The ${source} tag has no suffix`);
    const prefix = this.tags[handle];
    if (prefix) {
      try {
        return prefix + decodeURIComponent(suffix);
      } catch (error) {
        onError(String(error));
        return null;
      }
    }
    if (handle === '!') return source; // local tag
    onError(`Could not resolve tag: ${source}`);
    return null;
  }
  /**
   * Given a fully resolved tag, returns its printable string form,
   * taking into account current tag prefixes and defaults.
   */
  tagString(tag) {
    for (const [handle, prefix] of Object.entries(this.tags)) {
      if (tag.startsWith(prefix)) return handle + escapeTagName(tag.substring(prefix.length));
    }
    return tag[0] === '!' ? tag : `!<${tag}>`;
  }
  toString(doc) {
    const lines = this.yaml.explicit ? [`%YAML ${this.yaml.version || '1.2'}`] : [];
    const tagEntries = Object.entries(this.tags);
    let tagNames;
    if (doc && tagEntries.length > 0 && isNode(doc.contents)) {
      const tags = {};
      visit(doc.contents, (_key, node) => {
        if (isNode(node) && node.tag) tags[node.tag] = true;
      });
      tagNames = Object.keys(tags);
    } else tagNames = [];
    for (const [handle, prefix] of tagEntries) {
      if (handle === '!!' && prefix === 'tag:yaml.org,2002:') continue;
      if (!doc || tagNames.some(tn => tn.startsWith(prefix))) lines.push(`%TAG ${handle} ${prefix}`);
    }
    return lines.join('\n');
  }
}
Directives.defaultYaml = {
  explicit: false,
  version: '1.2'
};
Directives.defaultTags = {
  '!!': 'tag:yaml.org,2002:'
};

export { Directives };
