import { Scalar } from '../nodes/Scalar.mjs';
import { foldFlowLines, FOLD_QUOTED, FOLD_FLOW, FOLD_BLOCK } from './foldFlowLines.mjs';

const getFoldOptions = ctx => ({
  indentAtStart: ctx.indentAtStart,
  lineWidth: ctx.options.lineWidth,
  minContentWidth: ctx.options.minContentWidth
});
// Also checks for lines starting with %, as parsing the output as YAML 1.1 will
// presume that's starting a new document.
const containsDocumentMarker = str => /^(%|---|\.\.\.)/m.test(str);
function lineLengthOverLimit(str, lineWidth, indentLength) {
  if (!lineWidth || lineWidth < 0) return false;
  const limit = lineWidth - indentLength;
  const strLen = str.length;
  if (strLen <= limit) return false;
  for (let i = 0, start = 0; i < strLen; ++i) {
    if (str[i] === '\n') {
      if (i - start > limit) return true;
      start = i + 1;
      if (strLen - start <= limit) return false;
    }
  }
  return true;
}
function doubleQuotedString(value, ctx) {
  const json = JSON.stringify(value);
  if (ctx.options.doubleQuotedAsJSON) return json;
  const {
    implicitKey
  } = ctx;
  const minMultiLineLength = ctx.options.doubleQuotedMinMultiLineLength;
  const indent = ctx.indent || (containsDocumentMarker(value) ? '  ' : '');
  let str = '';
  let start = 0;
  for (let i = 0, ch = json[i]; ch; ch = json[++i]) {
    if (ch === ' ' && json[i + 1] === '\\' && json[i + 2] === 'n') {
      // space before newline needs to be escaped to not be folded
      str += json.slice(start, i) + '\\ ';
      i += 1;
      start = i;
      ch = '\\';
    }
    if (ch === '\\') switch (json[i + 1]) {
      case 'u':
        {
          str += json.slice(start, i);
          const code = json.substr(i + 2, 4);
          switch (code) {
            case '0000':
              str += '\\0';
              break;
            case '0007':
              str += '\\a';
              break;
            case '000b':
              str += '\\v';
              break;
            case '001b':
              str += '\\e';
              break;
            case '0085':
              str += '\\N';
              break;
            case '00a0':
              str += '\\_';
              break;
            case '2028':
              str += '\\L';
              break;
            case '2029':
              str += '\\P';
              break;
            default:
              if (code.substr(0, 2) === '00') str += '\\x' + code.substr(2);else str += json.substr(i, 6);
          }
          i += 5;
          start = i + 1;
        }
        break;
      case 'n':
        if (implicitKey || json[i + 2] === '"' || json.length < minMultiLineLength) {
          i += 1;
        } else {
          // folding will eat first newline
          str += json.slice(start, i) + '\n\n';
          while (json[i + 2] === '\\' && json[i + 3] === 'n' && json[i + 4] !== '"') {
            str += '\n';
            i += 2;
          }
          str += indent;
          // space after newline needs to be escaped to not be folded
          if (json[i + 2] === ' ') str += '\\';
          i += 1;
          start = i + 1;
        }
        break;
      default:
        i += 1;
    }
  }
  str = start ? str + json.slice(start) : json;
  return implicitKey ? str : foldFlowLines(str, indent, FOLD_QUOTED, getFoldOptions(ctx));
}
function singleQuotedString(value, ctx) {
  if (ctx.options.singleQuote === false || ctx.implicitKey && value.includes('\n') || /[ \t]\n|\n[ \t]/.test(value) // single quoted string can't have leading or trailing whitespace around newline
  ) return doubleQuotedString(value, ctx);
  const indent = ctx.indent || (containsDocumentMarker(value) ? '  ' : '');
  const res = "'" + value.replace(/'/g, "''").replace(/\n+/g, `$&\n${indent}`) + "'";
  return ctx.implicitKey ? res : foldFlowLines(res, indent, FOLD_FLOW, getFoldOptions(ctx));
}
function quotedString(value, ctx) {
  const {
    singleQuote
  } = ctx.options;
  let qs;
  if (singleQuote === false) qs = doubleQuotedString;else {
    const hasDouble = value.includes('"');
    const hasSingle = value.includes("'");
    if (hasDouble && !hasSingle) qs = singleQuotedString;else if (hasSingle && !hasDouble) qs = doubleQuotedString;else qs = singleQuote ? singleQuotedString : doubleQuotedString;
  }
  return qs(value, ctx);
}
function blockString({
  comment,
  type,
  value
}, ctx, onComment, onChompKeep) {
  const {
    blockQuote,
    commentString,
    lineWidth
  } = ctx.options;
  // 1. Block can't end in whitespace unless the last line is non-empty.
  // 2. Strings consisting of only whitespace are best rendered explicitly.
  if (!blockQuote || /\n[\t ]+$/.test(value) || /^\s*$/.test(value)) {
    return quotedString(value, ctx);
  }
  const indent = ctx.indent || (ctx.forceBlockIndent || containsDocumentMarker(value) ? '  ' : '');
  const literal = blockQuote === 'literal' ? true : blockQuote === 'folded' || type === Scalar.BLOCK_FOLDED ? false : type === Scalar.BLOCK_LITERAL ? true : !lineLengthOverLimit(value, lineWidth, indent.length);
  if (!value) return literal ? '|\n' : '>\n';
  // determine chomping from whitespace at value end
  let chomp;
  let endStart;
  for (endStart = value.length; endStart > 0; --endStart) {
    const ch = value[endStart - 1];
    if (ch !== '\n' && ch !== '\t' && ch !== ' ') break;
  }
  let end = value.substring(endStart);
  const endNlPos = end.indexOf('\n');
  if (endNlPos === -1) {
    chomp = '-'; // strip
  } else if (value === end || endNlPos !== end.length - 1) {
    chomp = '+'; // keep
    if (onChompKeep) onChompKeep();
  } else {
    chomp = ''; // clip
  }

  if (end) {
    value = value.slice(0, -end.length);
    if (end[end.length - 1] === '\n') end = end.slice(0, -1);
    end = end.replace(/\n+(?!\n|$)/g, `$&${indent}`);
  }
  // determine indent indicator from whitespace at value start
  let startWithSpace = false;
  let startEnd;
  let startNlPos = -1;
  for (startEnd = 0; startEnd < value.length; ++startEnd) {
    const ch = value[startEnd];
    if (ch === ' ') startWithSpace = true;else if (ch === '\n') startNlPos = startEnd;else break;
  }
  let start = value.substring(0, startNlPos < startEnd ? startNlPos + 1 : startEnd);
  if (start) {
    value = value.substring(start.length);
    start = start.replace(/\n+/g, `$&${indent}`);
  }
  const indentSize = indent ? '2' : '1'; // root is at -1
  let header = (literal ? '|' : '>') + (startWithSpace ? indentSize : '') + chomp;
  if (comment) {
    header += ' ' + commentString(comment.replace(/ ?[\r\n]+/g, ' '));
    if (onComment) onComment();
  }
  if (literal) {
    value = value.replace(/\n+/g, `$&${indent}`);
    return `${header}\n${indent}${start}${value}${end}`;
  }
  value = value.replace(/\n+/g, '\n$&').replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g, '$1$2') // more-indented lines aren't folded
  //                ^ more-ind. ^ empty     ^ capture next empty lines only at end of indent
  .replace(/\n+/g, `$&${indent}`);
  const body = foldFlowLines(`${start}${value}${end}`, indent, FOLD_BLOCK, getFoldOptions(ctx));
  return `${header}\n${indent}${body}`;
}
function plainString(item, ctx, onComment, onChompKeep) {
  const {
    type,
    value
  } = item;
  const {
    actualString,
    implicitKey,
    indent,
    inFlow
  } = ctx;
  if (implicitKey && /[\n[\]{},]/.test(value) || inFlow && /[[\]{},]/.test(value)) {
    return quotedString(value, ctx);
  }
  if (!value || /^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(value)) {
    // not allowed:
    // - empty string, '-' or '?'
    // - start with an indicator character (except [?:-]) or /[?-] /
    // - '\n ', ': ' or ' \n' anywhere
    // - '#' not preceded by a non-space char
    // - end with ' ' or ':'
    return implicitKey || inFlow || !value.includes('\n') ? quotedString(value, ctx) : blockString(item, ctx, onComment, onChompKeep);
  }
  if (!implicitKey && !inFlow && type !== Scalar.PLAIN && value.includes('\n')) {
    // Where allowed & type not set explicitly, prefer block style for multiline strings
    return blockString(item, ctx, onComment, onChompKeep);
  }
  if (indent === '' && containsDocumentMarker(value)) {
    ctx.forceBlockIndent = true;
    return blockString(item, ctx, onComment, onChompKeep);
  }
  const str = value.replace(/\n+/g, `$&\n${indent}`);
  // Verify that output will be parsed as a string, as e.g. plain numbers and
  // booleans get parsed with those types in v1.2 (e.g. '42', 'true' & '0.9e-3'),
  // and others in v1.1.
  if (actualString) {
    const test = tag => tag.default && tag.tag !== 'tag:yaml.org,2002:str' && tag.test?.test(str);
    const {
      compat,
      tags
    } = ctx.doc.schema;
    if (tags.some(test) || compat?.some(test)) return quotedString(value, ctx);
  }
  return implicitKey ? str : foldFlowLines(str, indent, FOLD_FLOW, getFoldOptions(ctx));
}
function stringifyString(item, ctx, onComment, onChompKeep) {
  const {
    implicitKey,
    inFlow
  } = ctx;
  const ss = typeof item.value === 'string' ? item : Object.assign({}, item, {
    value: String(item.value)
  });
  let {
    type
  } = item;
  if (type !== Scalar.QUOTE_DOUBLE) {
    // force double quotes on control characters & unpaired surrogates
    if (/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(ss.value)) type = Scalar.QUOTE_DOUBLE;
  }
  const _stringify = _type => {
    switch (_type) {
      case Scalar.BLOCK_FOLDED:
      case Scalar.BLOCK_LITERAL:
        return implicitKey || inFlow ? quotedString(ss.value, ctx) // blocks are not valid inside flow containers
        : blockString(ss, ctx, onComment, onChompKeep);
      case Scalar.QUOTE_DOUBLE:
        return doubleQuotedString(ss.value, ctx);
      case Scalar.QUOTE_SINGLE:
        return singleQuotedString(ss.value, ctx);
      case Scalar.PLAIN:
        return plainString(ss, ctx, onComment, onChompKeep);
      default:
        return null;
    }
  };
  let res = _stringify(type);
  if (res === null) {
    const {
      defaultKeyType,
      defaultStringType
    } = ctx.options;
    const t = implicitKey && defaultKeyType || defaultStringType;
    res = _stringify(t);
    if (res === null) throw new Error(`Unsupported default string type ${t}`);
  }
  return res;
}

export { stringifyString };
