/* @flow strict */
/* eslint-disable no-param-reassign, no-lonely-if */

import type {
  FieldNode,
  FragmentDefinitionNode,
  FragmentSpreadNode,
  InlineFragmentNode,
  GraphQLResolveInfo,
  GraphQLOutputType,
} from '../graphql';
import {
  Kind,
  GraphQLObjectType,
  GraphQLList,
  GraphQLNonNull,
  GraphQLInterfaceType,
} from '../graphql';
import { deepmerge } from './deepmerge';

const { FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT } = Kind;

// export type ProjectionType = { [fieldName: string]: $Shape<ProjectionNode> | true };
// export type ProjectionNode = { [fieldName: string]: $Shape<ProjectionNode> } | true;
export type ProjectionType = { [fieldName: string]: any };
export type ProjectionNode = { [fieldName: string]: any };

export function getProjectionFromAST(
  info: GraphQLResolveInfo,
  fieldNode?: FieldNode | InlineFragmentNode | FragmentDefinitionNode
): ProjectionType {
  if (!info) {
    return {};
  }

  const queryProjection = getProjectionFromASTquery(info, fieldNode);
  const queryExtProjection = extendByFieldProjection(info.returnType, queryProjection);
  return queryExtProjection;
}

export function getProjectionFromASTquery(
  info: GraphQLResolveInfo,
  fieldNode?: FieldNode | InlineFragmentNode | FragmentDefinitionNode
): ProjectionType {
  if (!info) {
    return {};
  }

  let selections; // Array<FieldNode | InlineFragmentNode | FragmentSpreadNode>;
  if (fieldNode) {
    if (fieldNode.selectionSet) {
      selections = fieldNode.selectionSet.selections;
    }
  } else if (Array.isArray(info.fieldNodes)) {
    // get all selectionSets
    selections = info.fieldNodes.reduce((result, source) => {
      if (source.selectionSet) {
        result.push(...source.selectionSet.selections);
      }
      return result;
    }, []);
  }

  const projection = (selections || []).reduce(
    (res, ast: FieldNode | InlineFragmentNode | FragmentSpreadNode) => {
      switch (ast.kind) {
        case FIELD: {
          const { value } = ast.name;
          if (res[value]) {
            res[value] = deepmerge(res[value], getProjectionFromASTquery(info, ast) || true);
          } else {
            res[value] = getProjectionFromASTquery(info, ast) || true;
          }
          return res;
        }
        case INLINE_FRAGMENT:
          return deepmerge(res, getProjectionFromASTquery(info, ast));
        case FRAGMENT_SPREAD:
          return deepmerge(res, getProjectionFromASTquery(info, info.fragments[ast.name.value]));
        default:
          throw new Error('Unsuported query selection');
      }
    },
    {}
  );

  return projection;
}

export function getFlatProjectionFromAST(
  info: GraphQLResolveInfo,
  fieldNodes?: FieldNode | InlineFragmentNode | FragmentDefinitionNode
) {
  const projection = getProjectionFromAST(info, fieldNodes) || {};
  const flatProjection = {};
  Object.keys(projection).forEach((key) => {
    flatProjection[key] = !!projection[key];
  });
  return flatProjection;
}

// This method traverse fields and extends current projection
// by projection from fields
export function extendByFieldProjection(
  returnType: GraphQLOutputType,
  projection: ProjectionType
): ProjectionType {
  let type: GraphQLOutputType = returnType;

  while (type instanceof GraphQLList || type instanceof GraphQLNonNull) {
    type = type.ofType;
  }

  if (!(type instanceof GraphQLObjectType || type instanceof GraphQLInterfaceType)) {
    return projection;
  }

  let proj = projection;
  Object.keys(proj).forEach((key) => {
    const field = (type: any).getFields()[key];
    if (!field) return;

    if (field.projection) proj = deepmerge(proj, field.projection);
    if (field.extensions && field.extensions.projection) {
      proj = deepmerge(proj, field.extensions.projection);
    }
    proj[key] = extendByFieldProjection((field.type: any), proj[key]);
  });

  return proj;
}

@@ -968,33 +968,33 @@
 rojectionFromAST
-q
+Q
 uery(info, field
@@ -1152,25 +1152,25 @@
 ctionFromAST
-q
+Q
 uery(%0A  info
@@ -2077,33 +2077,33 @@
 rojectionFromAST
-q
+Q
 uery(info, ast) 
@@ -2168,33 +2168,33 @@
 rojectionFromAST
-q
+Q
 uery(info, ast) 
@@ -2320,33 +2320,33 @@
 rojectionFromAST
-q
+Q
 uery(info, ast))
@@ -2429,17 +2429,17 @@
 nFromAST
-q
+Q
 uery(inf
@@ -2524,16 +2524,17 @@
 r('Unsup
+p
 orted qu
@@ -2592,32 +2592,176 @@
 rn projection;%0A%7D
+%0A// export old getProjectionFromASTquery to be removed in next major release%0Aexport const getProjectionFromASTquery = getProjectionFromASTQuery;
 %0A%0Aexport functio

fix typos in projection.ts (#288)