import {
  DocumentNode,
  OperationDefinitionNode,
  FragmentDefinitionNode,
  ValueNode,
} from 'graphql';
import { assign } from './util/assign';

import { valueToObjectRepresentation, JsonValue } from './storeUtils';

export function getMutationDefinition(
  doc: DocumentNode,
): OperationDefinitionNode {
  checkDocument(doc);

  let mutationDef: OperationDefinitionNode | null = doc.definitions.filter(
    definition =>
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'mutation',
  )[0] as OperationDefinitionNode;

  if (!mutationDef) {
    throw new Error('Must contain a mutation definition.');
  }

  return mutationDef;
}

// Checks the document for errors and throws an exception if there is an error.
export function checkDocument(doc: DocumentNode) {
  if (doc.kind !== 'Document') {
    throw new Error(`Expecting a parsed GraphQL document. Perhaps you need to wrap the query \
string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`);
  }

  const operations = doc.definitions
    .filter(d => d.kind !== 'FragmentDefinition')
    .map(definition => {
      if (definition.kind !== 'OperationDefinition') {
        throw new Error(
          `Schema type definitions not allowed in queries. Found: "${
            definition.kind
          }"`,
        );
      }
      return definition;
    });

  if (operations.length > 1) {
    throw new Error(
      `Ambiguous GraphQL document: contains ${operations.length} operations`,
    );
  }
}

export function getOperationDefinition(
  doc: DocumentNode,
): OperationDefinitionNode | undefined {
  checkDocument(doc);
  return doc.definitions.filter(
    definition => definition.kind === 'OperationDefinition',
  )[0] as OperationDefinitionNode;
}

export function getOperationDefinitionOrDie(
  document: DocumentNode,
): OperationDefinitionNode {
  const def = getOperationDefinition(document);
  if (!def) {
    throw new Error(`GraphQL document is missing an operation`);
  }
  return def;
}

export function getOperationName(doc: DocumentNode): string | null {
  return (
    doc.definitions
      .filter(
        definition =>
          definition.kind === 'OperationDefinition' && definition.name,
      )
      .map((x: OperationDefinitionNode) => x.name.value)[0] || null
  );
}

// Returns the FragmentDefinitions from a particular document as an array
export function getFragmentDefinitions(
  doc: DocumentNode,
): FragmentDefinitionNode[] {
  return doc.definitions.filter(
    definition => definition.kind === 'FragmentDefinition',
  ) as FragmentDefinitionNode[];
}

export function getQueryDefinition(doc: DocumentNode): OperationDefinitionNode {
  const queryDef = getOperationDefinition(doc) as OperationDefinitionNode;

  if (!queryDef || queryDef.operation !== 'query') {
    throw new Error('Must contain a query definition.');
  }

  return queryDef;
}

export function getFragmentDefinition(
  doc: DocumentNode,
): FragmentDefinitionNode {
  if (doc.kind !== 'Document') {
    throw new Error(`Expecting a parsed GraphQL document. Perhaps you need to wrap the query \
string in a "gql" tag? http://docs.apollostack.com/apollo-client/core.html#gql`);
  }

  if (doc.definitions.length > 1) {
    throw new Error('Fragment must have exactly one definition.');
  }

  const fragmentDef = doc.definitions[0] as FragmentDefinitionNode;

  if (fragmentDef.kind !== 'FragmentDefinition') {
    throw new Error('Must be a fragment definition.');
  }

  return fragmentDef as FragmentDefinitionNode;
}

/**
 * Returns the first operation definition found in this document.
 * If no operation definition is found, the first fragment definition will be returned.
 * If no definitions are found, an error will be thrown.
 */
export function getMainDefinition(
  queryDoc: DocumentNode,
): OperationDefinitionNode | FragmentDefinitionNode {
  checkDocument(queryDoc);

  let fragmentDefinition;

  for (let definition of queryDoc.definitions) {
    if (definition.kind === 'OperationDefinition') {
      const operation = (definition as OperationDefinitionNode).operation;
      if (
        operation === 'query' ||
        operation === 'mutation' ||
        operation === 'subscription'
      ) {
        return definition as OperationDefinitionNode;
      }
    }
    if (definition.kind === 'FragmentDefinition' && !fragmentDefinition) {
      // we do this because we want to allow multiple fragment definitions
      // to precede an operation definition.
      fragmentDefinition = definition as FragmentDefinitionNode;
    }
  }

  if (fragmentDefinition) {
    return fragmentDefinition;
  }

  throw new Error(
    'Expected a parsed GraphQL query with a query, mutation, subscription, or a fragment.',
  );
}

/**
 * This is an interface that describes a map from fragment names to fragment definitions.
 */
export interface FragmentMap {
  [fragmentName: string]: FragmentDefinitionNode;
}

// Utility function that takes a list of fragment definitions and makes a hash out of them
// that maps the name of the fragment to the fragment definition.
export function createFragmentMap(
  fragments: FragmentDefinitionNode[] = [],
): FragmentMap {
  const symTable: FragmentMap = {};
  fragments.forEach(fragment => {
    symTable[fragment.name.value] = fragment;
  });

  return symTable;
}

export function getDefaultValues(
  definition: OperationDefinitionNode | undefined,
): { [key: string]: JsonValue } {
  if (
    definition &&
    definition.variableDefinitions &&
    definition.variableDefinitions.length
  ) {
    const defaultValues = definition.variableDefinitions
      .filter(({ defaultValue }) => defaultValue)
      .map(
        ({ variable, defaultValue }): { [key: string]: JsonValue } => {
          const defaultValueObj: { [key: string]: JsonValue } = {};
          valueToObjectRepresentation(
            defaultValueObj,
            variable.name,
            defaultValue as ValueNode,
          );

          return defaultValueObj;
        },
      );

    return assign({}, ...defaultValues);
  }

  return {};
}

/**
 * Returns the names of all variables declared by the operation.
 */
export function variablesInOperation(
  operation: OperationDefinitionNode,
): Set<string> {
  const names = new Set<string>();
  if (operation.variableDefinitions) {
    for (const definition of operation.variableDefinitions) {
      names.add(definition.variable.name.value);
    }
  }

  return names;
}
