import { ApolloQueryResult } from "apollo-client";
import { DocumentNode } from "graphql";
import gql from "graphql-tag";
import React from "react";
import { Query, QueryResult } from "react-apollo";
import { useIntl } from "react-intl";

import useAppState from "./hooks/useAppState";
import useNotifier from "./hooks/useNotifier";
import { commonMessages } from "./intl";
import { maybe, RequireAtLeastOne } from "./misc";

export interface LoadMore<TData, TVariables> {
  loadMore: (
    mergeFunc: (prev: TData, next: TData) => TData,
    extraVariables: Partial<TVariables>
  ) => Promise<ApolloQueryResult<TData>>;
}

export type TypedQueryResult<TData, TVariables> = QueryResult<
  TData,
  TVariables
> &
  LoadMore<TData, TVariables>;

export interface TypedQueryInnerProps<TData, TVariables> {
  children: (result: TypedQueryResult<TData, TVariables>) => React.ReactNode;
  displayLoader?: boolean;
  skip?: boolean;
  variables?: TVariables;
  require?: Array<keyof TData>;
}

interface QueryProgressProps {
  loading: boolean;
  onLoading: () => void;
  onCompleted: () => void;
}

class QueryProgress extends React.Component<QueryProgressProps, {}> {
  componentDidMount() {
    const { loading, onLoading } = this.props;
    if (loading) {
      onLoading();
    }
  }

  componentDidUpdate(prevProps) {
    const { loading, onLoading, onCompleted } = this.props;
    if (prevProps.loading !== loading) {
      if (loading) {
        onLoading();
      } else {
        onCompleted();
      }
    }
  }

  render() {
    return this.props.children;
  }
}

// For some reason Query returns () => Element instead of () => ReactNode
export function TypedQuery<TData, TVariables>(
  query: DocumentNode
): React.FC<TypedQueryInnerProps<TData, TVariables>> {
  return ({ children, displayLoader, skip, variables, require }) => {
    const pushMessage = useNotifier();
    const [, dispatchAppState] = useAppState();
    const intl = useIntl();

    return (
      <Query
        fetchPolicy="cache-and-network"
        query={query}
        variables={variables}
        skip={skip}
        context={{ useBatching: true }}
        errorPolicy="all"
      >
        {(queryData: QueryResult<TData, TVariables>) => {
          if (queryData.error) {
            if (
              !queryData.error.graphQLErrors.every(
                err =>
                  maybe(() => err.extensions.exception.code) ===
                  "PermissionDenied"
              )
            ) {
              pushMessage({
                text: intl.formatMessage(commonMessages.somethingWentWrong)
              });
            }
          }

          const loadMore = (
            mergeFunc: (
              previousResults: TData,
              fetchMoreResult: TData
            ) => TData,
            extraVariables: RequireAtLeastOne<TVariables>
          ) =>
            queryData.fetchMore({
              query,
              updateQuery: (previousResults, { fetchMoreResult }) => {
                if (!fetchMoreResult) {
                  return previousResults;
                }
                return mergeFunc(previousResults, fetchMoreResult);
              },
              variables: { ...variables, ...extraVariables }
            });

          if (
            !queryData.loading &&
            require &&
            queryData.data &&
            !require.reduce(
              (acc, key) => acc && queryData.data[key] !== null,
              true
            )
          ) {
            dispatchAppState({
              payload: {
                error: "not-found"
              },
              type: "displayError"
            });
          }

          if (displayLoader) {
            return (
              <QueryProgress
                loading={queryData.loading}
                onCompleted={() =>
                  dispatchAppState({
                    payload: {
                      value: false
                    },
                    type: "displayLoader"
                  })
                }
                onLoading={() =>
                  dispatchAppState({
                    payload: {
                      value: true
                    },
                    type: "displayLoader"
                  })
                }
              >
                {children({
                  ...queryData,
                  loadMore
                })}
              </QueryProgress>
            );
          }

          return (
            <>
              {children({
                ...queryData,
                loadMore
              })}
            </>
          );
        }}
      </Query>
    );
  };
}

export const pageInfoFragment = gql`
  fragment PageInfoFragment on PageInfo {
    endCursor
    hasNextPage
    hasPreviousPage
    startCursor
  }
`;
