/**
 *  Copyright (c) 2019 GraphQL Contributors
 *  All rights reserved.
 *
 *  This source code is licensed under the license found in the
 *  LICENSE file in the root directory of this source tree.
 *
 *  @flow
 */

/**
 * Ported from codemirror-graphql
 * https://github.com/graphql/codemirror-graphql/blob/master/src/info.js
 */

import type { GraphQLSchema } from 'graphql';
import type { ContextToken } from 'graphql-language-service-types';
import type { Hover } from 'vscode-languageserver-types';
import type { Position } from 'graphql-language-service-utils';
import { getTokenAtPosition, getTypeInfo } from './getAutocompleteSuggestions';
import { GraphQLNonNull, GraphQLList } from 'graphql';

export function getHoverInformation(
  schema: GraphQLSchema,
  queryText: string,
  cursor: Position,
  contextToken?: ContextToken,
): Hover.contents {
  const token = contextToken || getTokenAtPosition(queryText, cursor);

  if (!schema || !token || !token.state) {
    return [];
  }

  const state = token.state;
  const kind = state.kind;
  const step = state.step;
  const typeInfo = getTypeInfo(schema, token.state);
  const options = { schema };

  // Given a Schema and a Token, produce the contents of an info tooltip.
  // To do this, create a div element that we will render "into" and then pass
  // it to various rendering functions.
  if (
    (kind === 'Field' && step === 0 && typeInfo.fieldDef) ||
    (kind === 'AliasedField' && step === 2 && typeInfo.fieldDef)
  ) {
    const into = [];
    renderField(into, typeInfo, options);
    renderDescription(into, options, typeInfo.fieldDef);
    return into.join('').trim();
  } else if (kind === 'Directive' && step === 1 && typeInfo.directiveDef) {
    const into = [];
    renderDirective(into, typeInfo, options);
    renderDescription(into, options, typeInfo.directiveDef);
    return into.join('').trim();
  } else if (kind === 'Argument' && step === 0 && typeInfo.argDef) {
    const into = [];
    renderArg(into, typeInfo, options);
    renderDescription(into, options, typeInfo.argDef);
    return into.join('').trim();
  } else if (
    kind === 'EnumValue' &&
    typeInfo.enumValue &&
    typeInfo.enumValue.description
  ) {
    const into = [];
    renderEnumValue(into, typeInfo, options);
    renderDescription(into, options, typeInfo.enumValue);
    return into.join('').trim();
  } else if (
    kind === 'NamedType' &&
    typeInfo.type &&
    typeInfo.type.description
  ) {
    const into = [];
    renderType(into, typeInfo, options, typeInfo.type);
    renderDescription(into, options, typeInfo.type);
    return into.join('').trim();
  }
}

function renderField(into, typeInfo, options) {
  renderQualifiedField(into, typeInfo, options);
  renderTypeAnnotation(into, typeInfo, options, typeInfo.type);
}

function renderQualifiedField(into, typeInfo, options) {
  if (!typeInfo.fieldDef) {
    return;
  }
  const fieldName = (typeInfo.fieldDef.name: string);
  if (fieldName.slice(0, 2) !== '__') {
    renderType(into, typeInfo, options, typeInfo.parentType);
    text(into, '.');
  }
  text(into, fieldName);
}

function renderDirective(into, typeInfo, options) {
  if (!typeInfo.directiveDef) {
    return;
  }
  const name = '@' + typeInfo.directiveDef.name;
  text(into, name);
}

function renderArg(into, typeInfo, options) {
  if (typeInfo.directiveDef) {
    renderDirective(into, typeInfo, options);
  } else if (typeInfo.fieldDef) {
    renderQualifiedField(into, typeInfo, options);
  }

  if (!typeInfo.argDef) {
    return;
  }

  const name = typeInfo.argDef.name;
  text(into, '(');
  text(into, name);
  renderTypeAnnotation(into, typeInfo, options, typeInfo.inputType);
  text(into, ')');
}

function renderTypeAnnotation(into, typeInfo, options, t) {
  text(into, ': ');
  renderType(into, typeInfo, options, t);
}

function renderEnumValue(into, typeInfo, options) {
  if (!typeInfo.enumValue) {
    return;
  }
  const name = typeInfo.enumValue.name;
  renderType(into, typeInfo, options, typeInfo.inputType);
  text(into, '.');
  text(into, name);
}

function renderType(into, typeInfo, options, t) {
  if (!t) {
    return;
  }
  if (t instanceof GraphQLNonNull) {
    renderType(into, typeInfo, options, t.ofType);
    text(into, '!');
  } else if (t instanceof GraphQLList) {
    text(into, '[');
    renderType(into, typeInfo, options, t.ofType);
    text(into, ']');
  } else {
    text(into, t.name);
  }
}

function renderDescription(into, options, def) {
  if (!def) {
    return;
  }
  const description =
    typeof def.description === 'string' ? def.description : null;
  if (description) {
    text(into, '\n\n');
    text(into, description);
  }
  renderDeprecation(into, options, def);
}

function renderDeprecation(into, options, def) {
  if (!def) {
    return;
  }
  const reason =
    typeof def.deprecationReason === 'string' ? def.deprecationReason : null;
  if (!reason) {
    return;
  }
  text(into, '\n\n');
  text(into, 'Deprecated: ');
  text(into, reason);
}

function text(into: string[], content: string) {
  into.push(content);
}
