import { Spec, Line, Tokens } from '../../primitives.js';
import { splitSpace, isSpace } from '../../util.js';
import { Tokenizer } from './index.js';

const isQuoted = (s: string) => s && s.startsWith('"') && s.endsWith('"');

/**
 * Splits remaining `spec.lines[].tokens.description` into `name` and `descriptions` tokens,
 * and populates the `spec.name`
 */
export default function nameTokenizer(): Tokenizer {
  const typeEnd = (num: number, { tokens }: Line, i: number) =>
    tokens.type === '' ? num : i;

  return (spec: Spec): Spec => {
    // look for the name starting in the line where {type} ends
    let finalTypeLine = spec.source.reduce(typeEnd, 0);

    let tokens: Tokens;
    if (spec.type) {
      do {
        ({ tokens } = spec.source[finalTypeLine]);
        if (tokens.description.trim()) {
          break;
        }
        finalTypeLine++;
      } while (spec.source[finalTypeLine]);
    } else {
      ({ tokens } = spec.source[finalTypeLine]);
    }

    const source = tokens.description.trimStart();

    const quotedGroups = source.split('"');

    // if it starts with quoted group, assume it is a literal
    if (
      quotedGroups.length > 1 &&
      quotedGroups[0] === '' &&
      quotedGroups.length % 2 === 1
    ) {
      spec.name = quotedGroups[1];
      tokens.name = `"${quotedGroups[1]}"`;
      [tokens.postName, tokens.description] = splitSpace(
        source.slice(tokens.name.length)
      );
      return spec;
    }

    let brackets = 0;
    let name = '';
    let optional = false;
    let defaultValue: string;

    // assume name is non-space string or anything wrapped into brackets
    for (const ch of source) {
      if (brackets === 0 && isSpace(ch)) break;
      if (ch === '[') brackets++;
      if (ch === ']') brackets--;
      name += ch;
    }

    if (brackets !== 0) {
      spec.problems.push({
        code: 'spec:name:unpaired-brackets',
        message: 'unpaired brackets',
        line: spec.source[0].number,
        critical: true,
      });
      return spec;
    }

    const nameToken = name;

    if (name[0] === '[' && name[name.length - 1] === ']') {
      optional = true;
      name = name.slice(1, -1);

      const parts = name.split('=');
      name = parts[0].trim();
      if (parts[1] !== undefined)
        defaultValue = parts.slice(1).join('=').trim();

      if (name === '') {
        spec.problems.push({
          code: 'spec:name:empty-name',
          message: 'empty name',
          line: spec.source[0].number,
          critical: true,
        });
        return spec;
      }

      if (defaultValue === '') {
        spec.problems.push({
          code: 'spec:name:empty-default',
          message: 'empty default value',
          line: spec.source[0].number,
          critical: true,
        });
        return spec;
      }

      // has "=" and is not a string, except for "=>"
      if (!isQuoted(defaultValue) && /=(?!>)/.test(defaultValue)) {
        spec.problems.push({
          code: 'spec:name:invalid-default',
          message: 'invalid default value syntax',
          line: spec.source[0].number,
          critical: true,
        });
        return spec;
      }
    }

    if (!optional) {
      const eqIndex = name.search(/=(?!>)/);
      if (eqIndex !== -1) {
        defaultValue = name.slice(eqIndex + 1).trim();
        name = name.slice(0, eqIndex).trim();

        if (name === '') {
          spec.problems.push({
            code: 'spec:name:empty-name',
            message: 'empty name',
            line: spec.source[0].number,
            critical: true,
          });
          return spec;
        }

        if (defaultValue === '') {
          spec.problems.push({
            code: 'spec:name:empty-default',
            message: 'empty default value',
            line: spec.source[0].number,
            critical: true,
          });
          return spec;
        }

        if (!isQuoted(defaultValue) && /=(?!>)/.test(defaultValue)) {
          spec.problems.push({
            code: 'spec:name:invalid-default',
            message: 'invalid default value syntax',
            line: spec.source[0].number,
            critical: true,
          });
          return spec;
        }
      }
    }

    spec.optional = optional;
    spec.name = name;
    tokens.name = nameToken;

    if (defaultValue !== undefined) spec.default = defaultValue;
    [tokens.postName, tokens.description] = splitSpace(
      source.slice(tokens.name.length)
    );
    return spec;
  };
}
