import { SyntaxNode, Argument, Parameter } from './python-parser';

const comma = ', ';

// tslint:disable-next-line: max-func-body-length
function printTabbed(node: SyntaxNode, tabLevel: number): string {
  const tabs = ' '.repeat(4 * tabLevel);
  switch (node.type) {
    case 'assert':
      return tabs + 'assert ' + printNode(node.cond);
    case 'assign':
      return (
        tabs +
        commaSep(node.targets) +
        ' ' +
        (node.op || '=') +
        ' ' +
        commaSep(node.sources)
      );
    case 'binop':
      return '(' + printNode(node.left) + node.op + printNode(node.right) + ')';
    case 'break':
      return tabs + 'break';
    case 'call':
      return printNode(node.func) + '(' + node.args.map(printArg) + ')';
    case 'class':
      return (
        tabs +
        'class ' +
        node.name +
        (node.extends ? '(' + commaSep(node.extends) + ')' : '') +
        ':' +
        lines(node.code, tabLevel + 1)
      );
    case 'comp_for':
    case 'comp_if':
    case 'continue':
      return tabs + 'continue';
    case 'decorator':
      return (
        '@' +
        node.decorator +
        (node.args ? '(' + commaSep(node.args) + ')' : '')
      );
    case 'decorate':
      return (
        tabs +
        lines(node.decorators, tabLevel) +
        printTabbed(node.def, tabLevel)
      );
    case 'def':
      return (
        tabs +
        'def ' +
        node.name +
        '(' +
        node.params.map(printParam).join(comma) +
        '):' +
        lines(node.code, tabLevel + 1)
      );
    case 'dict':
      return '{' + node.entries.map(e => e.k + ':' + e.v) + '}';
    case 'dot':
      return printNode(node.value) + '.' + node.name;
    case 'else':
      return tabs + 'else:' + lines(node.code, tabLevel + 1);
    case 'for':
      return (
        tabs +
        'for ' +
        commaSep(node.target) +
        ' in ' +
        commaSep(node.iter) +
        ':' +
        lines(node.code, tabLevel + 1) +
        (node.else ? lines(node.else, tabLevel + 1) : '')
      );
    case 'from':
      return (
        tabs +
        'from ' +
        node.base +
        ' import ' +
        node.imports
          .map(im => im.path + (im.name ? ' as ' + im.name : ''))
          .join(comma)
      );
    case 'global':
      if (typeof(node.names) == 'undefined' || typeof(node.names) == 'string') {
        return tabs + 'global ' + node.names;
      } else {
        return tabs + 'global ' + node.names.join(comma);
      }
    case 'if':
      return (
        tabs +
        'if ' +
        printNode(node.cond) +
        ':' +
        lines(node.code, tabLevel + 1) +
        (node.elif
          ? node.elif.map(
              elif =>
                tabs +
                'elif ' +
                elif.cond +
                ':' +
                lines(elif.code, tabLevel + 1)
            )
          : '') +
        (node.else ? tabs + 'else:' + lines(node.else.code, tabLevel + 1) : '')
      );
    case 'ifexpr':
      return (
        printNode(node.then) +
        ' if ' +
        printNode(node.test) +
        ' else ' +
        printNode(node.else)
      );
    case 'import':
      return (
        tabs +
        'import ' +
        node.names
          .map(n => n.path + (n.name ? ' as ' + n.name : ''))
          .join(comma)
      );
    case 'index':
      return printNode(node.value) + '[' + commaSep(node.args) + ']';
    case 'lambda':
      return (
        'lambda ' +
        node.args.map(printParam).join(comma) +
        ': ' +
        printNode(node.code)
      );
    case 'list':
      return '[' + node.items.map(item => printNode(item)).join(comma) + ']';
    case 'literal':
      return typeof node.value === 'string' && node.value.indexOf('\n') >= 0
        ? '""' + node.value + '""'
        : node.value.toString();
    case 'module':
      return lines(node.code, tabLevel);
    case 'name':
      return node.id;
    case 'nonlocal':
      return tabs + 'nonlocal ' + node.names.join(comma);
    case 'raise':
      return tabs + 'raise ' + printNode(node.err);
    case 'return':
      return tabs + 'return ' + (node.values ? commaSep(node.values) : '');
    case 'set':
      return '{' + commaSep(node.entries) + '}';
    case 'slice':
      return (
        (node.start ? printNode(node.start) : '') +
        ':' +
        (node.stop ? printNode(node.stop) : '') +
        (node.step ? ':' + printNode(node.step) : '')
      );
    case 'starred':
      return '*' + printNode(node.value);
    case 'try':
      return (
        tabs +
        'try:' +
        lines(node.code, tabLevel + 1) +
        (node.excepts
          ? node.excepts.map(
              ex =>
                tabs +
                'except ' +
                (ex.cond
                  ? printNode(ex.cond) + (ex.name ? ' as ' + ex.name : '')
                  : '') +
                ':' +
                lines(ex.code, tabLevel + 1)
            )
          : '') +
        (node.else ? tabs + 'else:' + lines(node.else, tabLevel + 1) : '') +
        (node.finally
          ? tabs + 'finally:' + lines(node.finally, tabLevel + 1)
          : '')
      );
    case 'tuple':
      return '(' + commaSep(node.items) + ')';
    case 'unop':
      return node.op + '(' + printNode(node.operand) + ')';
    case 'while':
      return (
        tabs +
        'while ' +
        printNode(node.cond) +
        ':' +
        lines(node.code, tabLevel + 1)
      );
    case 'with':
      return (
        tabs +
        'with ' +
        node.items.map(w => w.with + (w.as ? ' as ' + w.as : '')).join(comma) +
        ':' +
        lines(node.code, tabLevel + 1)
      );
    case 'yield':
      return (
        tabs +
        'yield ' +
        (node.from ? printNode(node.from) : '') +
        (node.value ? commaSep(node.value) : '')
      );
  }
}

function printParam(param: Parameter): string {
  return (
    (param.star ? '*' : '') +
    (param.starstar ? '**' : '') +
    param.name +
    (param.default_value ? '=' + printNode(param.default_value) : '') +
    (param.anno ? printNode(param.anno) : '')
  );
}

function printArg(arg: Argument): string {
  return (
    (arg.kwargs ? '**' : '') +
    (arg.varargs ? '*' : '') +
    (arg.keyword ? printNode(arg.keyword) + '=' : '') +
    printNode(arg.actual) +
    (arg.loop ? ' for ' + arg.loop.for + ' in ' + arg.loop.in : '')
  );
}

function commaSep(items: SyntaxNode[]): string {
  return items.map(printNode).join(comma);
}

function lines(items: SyntaxNode[], tabLevel: number): string {
  return items
    .map(i => printTabbed(i, tabLevel))
    .join(tabLevel === 0 ? '\n\n' : '\n'); // seperate top-level definitons with an extra newline
}

export function printNode(node: SyntaxNode): string {
  return printTabbed(node, 0);
}
