import { visit, ASTReducer } from 'graphql/language/visitor';
import type { ASTNode } from 'graphql/language/ast';
import { prettify } from './prettier';
/**
 * Converts an AST into a string, using one set of reasonable
 * formatting rules.
 */

export type PrintOptions = {
  clientVarName?: string;
  transformVariableName?: (x: string) => string;
  thenCode?: string;
};

export function printJS(
  ast: ASTNode,
  options: PrintOptions = {},
  simplified = true
): string {
  const str = visit<string>(ast, printDocASTReducer(simplified)(options));
  return str;
}

const printDocASTReducer = (simplified: boolean) => {
  const importList: Set<string> = new Set();
  return ({
    clientVarName = 'client',
    transformVariableName = (x) => x,
    thenCode,
  }: PrintOptions) =>
    ({
      Name: { leave: (node) => node.value },
      Variable: { leave: (node) => transformVariableName(node.name) },
      NamedType: {
        leave: ({ name }) => {
          importList.add(name);
          return name;
        },
      },
      ListType: { leave: ({ type }) => type + '[]' },
      NonNullType: { leave: ({ type }) => type },
      Directive: { leave: ({ name, arguments: args }) => '' },

      IntValue: { leave: ({ value }) => value },
      FloatValue: { leave: ({ value }) => value },
      StringValue: {
        leave: ({ value, block: isBlockString }, key) => JSON.stringify(value),
      },
      BooleanValue: { leave: ({ value }) => (value ? 'true' : 'false') },
      NullValue: { leave: () => 'null' },
      EnumValue: { leave: ({ value }) => `'${value}'` },
      ListValue: {
        leave: ({ values }) => '[' + join(values.slice(), ', ') + ']',
      },
      ObjectValue: {
        leave: ({ fields }) => '{' + join(fields.slice(), ', ') + '}',
      },
      ObjectField: { leave: ({ name, value }) => name + ': ' + value },

      // Document

      Document: {
        leave: (node) => join(node.definitions.slice(), '\n\n') + '\n',
      },

      OperationDefinition: {
        leave: (node) => {
          const selectionSet = node.selectionSet;
          // Anonymous queries with no directives or variable definitions can use
          // the query short form.
          let code = join((node.variableDefinitions || []).slice(), '\n');
          if ((node.variableDefinitions || []).length) {
            code = `import { ${[...importList].join(
              ', '
            )} } from "@cognite/fdm-client";\n\n`;
            code = '// arguments\n' + code;
            code += '\n\n';
          }
          code += `${clientVarName}.${node.operation}(` + selectionSet + ')';
          if (thenCode) {
            code += `.then(${thenCode})`;
          }
          return prettify(code, 'typescript');
        },
      },

      VariableDefinition: {
        leave: ({ variable, type, defaultValue, directives }) => {
          return 'let ' + variable.replace('$', '') + `: ${type}`;
        },
      },
      SelectionSet: {
        leave: ({ selections }) => block(selections.slice()),
      },

      Field: {
        leave: ({ alias, name, arguments: args, directives, selectionSet }) => {
          if (args!.length === 0 && !join(selectionSet ? [selectionSet] : [])) {
            return name + ': true';
          }
          if (args!.length === 0) {
            console.log(name, args, selectionSet);
            return simplified && !selectionSet?.includes('{...everything}')
              ? `${name}: {...everything}`
              : name + ': ' + join(selectionSet ? [selectionSet] : []);
          }
          const argsAndFields = join([
            block((args || []).slice()),
            ',',
            selectionSet || '',
          ]);
          return name + ': ' + wrap('[', argsAndFields, ']');
        },
      },
      // join(directives, ' '),

      Argument: {
        leave: ({ name, value = '' }) => {
          if (typeof value === 'string') {
            return name + ': ' + transformVariableName(value.replace('$', ''));
          }
          console.error(
            `unhandled type, received ${JSON.stringify(value)} as Argument`
          );
          return '';
        },
      },
      // Fragments

      FragmentSpread: {
        leave: ({ name, directives }) => {
          // TODO FragmentSpread
          return '...' + name + ',';
        },
      },

      InlineFragment: {
        leave: ({ typeCondition, directives, selectionSet }) => {
          console.log({ selectionSet, directives, typeCondition });
          return join(['', wrap('on_', typeCondition), ':', selectionSet], ' ');
        },
      },

      FragmentDefinition: {
        leave: ({
          name,
          typeCondition,
          variableDefinitions,
          directives,
          selectionSet,
        }) => {
          // TODO FragmentDefinition
          // Note: fragment variable definitions are experimental and may be changed
          // or removed in the future.
          return `const ${name} = ` + selectionSet;
        },
      },
      // Directive
    } as ASTReducer<string>);
};

/**
 * Given maybeArray, print an empty string if it is null or empty, otherwise
 * print all items together separated by separator if provided
 */
function join(maybeArray: Array<string>, separator = '') {
  return maybeArray?.filter((x) => x).join(separator) ?? '';
}

/**
 * Given array, print each item on its own line, wrapped in an
 * indented "{ }" block.
 */
function block(array: any[]) {
  return array && array.length !== 0
    ? '{\n' + indent(join(array, ',\n')) + '\n}'
    : '';
}

/**
 * If maybeString is not null or empty, then wrap with start and end, otherwise
 * print an empty string.
 */
function wrap(start: any, maybeString: any, end = '') {
  return maybeString ? start + maybeString + end : '';
}

function indent(maybeString: any) {
  return maybeString && '  ' + maybeString.replace(/\n/g, '\n  ');
}

function isMultiline(string: any) {
  return string.indexOf('\n') !== -1;
}

function hasMultilineItems(maybeArray: any[]) {
  return maybeArray && maybeArray.some(isMultiline);
}
