'use strict';

const {OPTIONAL} = require('./NodeType');
const {OptionalTypeSyntax, VariadicTypeSyntax} = require('./SyntaxType');
const {format} = require('util');

/** @typedef {import('./parsing').AstNode} AstNode */
/** @typedef {(node) => string} ConcretePublish */
/** @typedef {{ [T in import('./NodeType').Type]: (node: AstNode, publish: ConcretePublish) => string }} Publisher */

/**
 * @param {AstNode} node
 * @param {Publisher} [opt_publisher]
 * @return {string}
 */
function publish(node, opt_publisher) {
  const publisher = opt_publisher || createDefaultPublisher();
  return publisher[node.type](node, function(childNode) {
    return publish(childNode, publisher);
  });
}

/**
 * @private
 * @param {string} str
 * @param {'none'|'single'|'double'|undefined} quoteStyle
 * @returns {string} Formatted string
 */
function addQuotesForName (str, quoteStyle) {
  // For `MemberName`, not strings
  if (!quoteStyle || quoteStyle === 'none') {
    return str;
  }
  const singleQuoteStyle = quoteStyle === 'single';

  return format(
    singleQuoteStyle
      ? "'%s'"
      : '"%s"',
    str
      .replace(/\\/g, '\\\\')
      .replace(
        singleQuoteStyle ? /'/gu : /"/gu,
        '\\' + (singleQuoteStyle ? "'" : '"')
      )
  );
}

/** @return {Publisher} */
function createDefaultPublisher() {
  return {
    NAME (nameNode) {
      return nameNode.name;
    },
    MEMBER (memberNode, concretePublish) {
      return format('%s.%s%s', concretePublish(memberNode.owner),
                    memberNode.hasEventPrefix ? 'event:' : '',
                    addQuotesForName(memberNode.name, memberNode.quoteStyle));
    },
    UNION (unionNode, concretePublish) {
      return format('%s|%s', concretePublish(unionNode.left),
                    concretePublish(unionNode.right));
    },
    VARIADIC (variadicNode, concretePublish) {
      if (variadicNode.meta.syntax === VariadicTypeSyntax.ONLY_DOTS) {
        return '...';
      }
      return format('...%s', concretePublish(variadicNode.value));
    },
    RECORD (recordNode, concretePublish) {
      const concretePublishedEntries = recordNode.entries.map(concretePublish);
      return format('{%s}', concretePublishedEntries.join(', '));
    },
    RECORD_ENTRY (entryNode, concretePublish) {
      if (!entryNode.value) return addQuotesForName(entryNode.key, entryNode.quoteStyle);
      const keySuffix = (
        entryNode.value.type === OPTIONAL &&
        entryNode.value.meta.syntax === OptionalTypeSyntax.SUFFIX_KEY_QUESTION_MARK
      )
        ? '?'
        : '';
      return format('%s%s: %s', addQuotesForName(entryNode.key, entryNode.quoteStyle), keySuffix, concretePublish(entryNode.value));
    },
    TUPLE (tupleNode, concretePublish) {
      const concretePublishedEntries = tupleNode.entries.map(concretePublish);
      return format('[%s]', concretePublishedEntries.join(', '));
    },
    GENERIC (genericNode, concretePublish) {
      const concretePublishedObjects = genericNode.objects.map(concretePublish);
      if (genericNode.meta) {
        switch (genericNode.meta.syntax) {
        case 'SQUARE_BRACKET':
          if (genericNode.subject && genericNode.subject.name === 'Array') {
            return format('%s[]', concretePublishedObjects.join(', '));
          }
          break;
        case 'ANGLE_BRACKET_WITH_DOT':
          return format('%s.<%s>', concretePublish(genericNode.subject),
                        concretePublishedObjects.join(', '));
        }
      }
      return format('%s<%s>', concretePublish(genericNode.subject),
                    concretePublishedObjects.join(', '));
    },
    MODULE (moduleNode, concretePublish) {
      return format('module:%s', concretePublish(moduleNode.value));
    },
    FILE_PATH (filePathNode) {
      return addQuotesForName(filePathNode.path, filePathNode.quoteStyle);
    },
    OPTIONAL (optionalNode, concretePublish) {
      if (optionalNode.meta.syntax === OptionalTypeSyntax.SUFFIX_KEY_QUESTION_MARK) {
        return concretePublish(optionalNode.value);
      }
      return format('%s=', concretePublish(optionalNode.value));
    },
    NULLABLE (nullableNode, concretePublish) {
      return format('?%s', concretePublish(nullableNode.value));
    },
    NOT_NULLABLE (notNullableNode, concretePublish) {
      return format('!%s', concretePublish(notNullableNode.value));
    },
    FUNCTION (functionNode, concretePublish) {
      const publidshedParams = functionNode.params.map(concretePublish);

      if (functionNode.new) {
        publidshedParams.unshift(format('new: %s',
          concretePublish(functionNode.new)));
      }

      if (functionNode.this) {
        publidshedParams.unshift(format('this: %s',
          concretePublish(functionNode.this)));
      }

      if (functionNode.returns) {
        return format('function(%s): %s', publidshedParams.join(', '),
                           concretePublish(functionNode.returns));
      }

      return format('function(%s)', publidshedParams.join(', '));
    },
    ARROW (functionNode, concretePublish) {
      const publishedParams = functionNode.params.map(concretePublish);
      return (functionNode.new ? 'new ' : '') + format('(%s) => %s', publishedParams.join(', '), concretePublish(functionNode.returns));
    },
    NAMED_PARAMETER (parameterNode, concretePublish) {
      return parameterNode.name + (parameterNode.typeName ? ': ' + concretePublish(parameterNode.typeName) : '');
    },
    ANY () {
      return '*';
    },
    UNKNOWN () {
      return '?';
    },
    INNER_MEMBER (memberNode, concretePublish) {
      return concretePublish(memberNode.owner) + '~' +
        (memberNode.hasEventPrefix ? 'event:' : '') +
        addQuotesForName(memberNode.name, memberNode.quoteStyle);
    },
    INSTANCE_MEMBER (memberNode, concretePublish) {
      return concretePublish(memberNode.owner) + '#' +
        (memberNode.hasEventPrefix ? 'event:' : '') +
        addQuotesForName(memberNode.name, memberNode.quoteStyle);
    },
    STRING_VALUE (stringValueNode) {
      return addQuotesForName(stringValueNode.string, stringValueNode.quoteStyle)
    },
    NUMBER_VALUE (numberValueNode) {
      return numberValueNode.number;
    },
    EXTERNAL (externalNode /* , concretePublish */) {
      const {name, quoteStyle} = externalNode;
      return format('external:%s', addQuotesForName(name, quoteStyle));
    },
    PARENTHESIS (parenthesizedNode, concretePublish) {
      return format('(%s)', concretePublish(parenthesizedNode.value));
    },
    TYPE_QUERY (typeQueryNode, concretePublish) {
      return format('typeof %s', concretePublish(typeQueryNode.name));
    },
    KEY_QUERY (keyQueryNode, concretePublish) {
      return format('keyof %s', concretePublish(keyQueryNode.value));
    },
    IMPORT (importNode, concretePublish) {
      return format('import(%s)', concretePublish(importNode.path));
    },
  };
}


module.exports = {
  publish: publish,
  createDefaultPublisher: createDefaultPublisher,
};
