
import { Children, ReactElement, ComponentClass, StatelessComponent } from 'react';
import * as ReactDOM from 'react-dom/server';
import ApolloClient, { ApolloQueryResult } from 'apollo-client';
const assign = require('object-assign');


export declare interface Context {
  client?: ApolloClient;
  store?: any;
  [key: string]: any;
}

export declare interface QueryTreeArgument {
  rootElement: ReactElement<any>;
  rootContext?: Context;
}

export declare interface QueryResult {
  query: Promise<ApolloQueryResult<any>>;
  element: ReactElement<any>;
  context: Context;
}

// Recurse a React Element tree, running visitor on each element.
// If visitor returns `false`, don't call the element's render function
//   or recurse into its child elements
export function walkTree(
  element: ReactElement<any>,
  context: Context,
  visitor: (element: ReactElement<any>, instance: any, context: Context) => boolean | void,
) {
  const Component = element.type;
  // a stateless functional component or a class
  if (typeof Component === 'function') {
    const props = assign({}, Component.defaultProps, element.props);
    let childContext = context;
    let child;

    // Are we are a react class?
    //   https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L66
    if (Component.prototype && Component.prototype.isReactComponent) {
      // typescript force casting since typescript doesn't have definitions for class
      // methods
      const _component = Component as any;
      const instance = new _component(props, context);
      // In case the user doesn't pass these to super in the constructor
      instance.props = instance.props || props;
      instance.context = instance.context || context;

      // Override setState to just change the state, not queue up an update.
      //   (we can't do the default React thing as we aren't mounted "properly"
      //   however, we don't need to re-render as well only support setState in
      //   componentWillMount, which happens *before* render).
      instance.setState = (newState) => {
        instance.state = assign({}, instance.state, newState);
      };

      // this is a poor man's version of
      //   https://github.com/facebook/react/blob/master/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js#L181
      if (instance.componentWillMount) {
        instance.componentWillMount();
      }

      if (instance.getChildContext) {
        childContext = assign({}, context, instance.getChildContext());
      }

      if (visitor(element, instance, context) === false) {
        return;
      }

      child = instance.render();
    } else { // just a stateless functional
      if (visitor(element, null, context) === false) {
        return;
      }

      // typescript casting for stateless component
      const _component = Component as StatelessComponent<any>;
      child = _component(props, context);
    }

    if (child) {
      walkTree(child, childContext, visitor);
    }
  } else { // a basic string or dom element, just get children
    if (visitor(element, null, context) === false) {
      return;
    }

    if (element.props && element.props.children) {
      Children.forEach(element.props.children, (child: any) => {
        if (child) {
          walkTree(child, context, visitor);
        }
      });
    }
  }
}

function getQueriesFromTree(
  { rootElement, rootContext = {} }: QueryTreeArgument, fetchRoot: boolean = true,
): QueryResult[] {
  const queries = [];

  walkTree(rootElement, rootContext, (element, instance, context) => {

    const skipRoot = !fetchRoot && (element === rootElement);
    if (instance && typeof instance.fetchData === 'function' && !skipRoot) {
      const query = instance.fetchData();
      if (query) {
        queries.push({ query, element, context });

        // Tell walkTree to not recurse inside this component;  we will
        // wait for the query to execute before attempting it.
        return false;
      }
    }
  });

  return queries;
}

// XXX component Cache
export function getDataFromTree(rootElement: ReactElement<any>, rootContext: any = {}, fetchRoot: boolean = true): Promise<void> {

  let queries = getQueriesFromTree({ rootElement, rootContext }, fetchRoot);

  // no queries found, nothing to do
  if (!queries.length) return Promise.resolve();

  const errors = [];
  // wait on each query that we found, re-rendering the subtree when it's done
  const mappedQueries = queries.map(({ query, element, context }) =>  {
    // we've just grabbed the query for element, so don't try and get it again
    return (
      query
        .then(_ => getDataFromTree(element, context, false))
        .catch(e => errors.push(e))
    );
  });

  // Run all queries. If there are errors, still wait for all queries to execute
  // so the caller can ignore them if they wish. See https://github.com/apollographql/react-apollo/pull/488#issuecomment-284415525
  return Promise.all(mappedQueries).then(_ => {
    if (errors.length > 0) {
      const error = errors.length === 1
        ? errors[0]
        : new Error(`${errors.length} errors were thrown when executing your GraphQL queries.`);
      error.queryErrors = errors;
      throw error;
    }
  });
}

export function renderToStringWithData(component: ReactElement<any>): Promise<string> {
  return getDataFromTree(component)
    .then(() => ReactDOM.renderToString(component));
}

export function cleanupApolloState(apolloState) {
  for (let queryId in apolloState.queries) {
    let fieldsToNotShip = ['minimizedQuery', 'minimizedQueryString'];
    for (let field of fieldsToNotShip) delete apolloState.queries[queryId][field];
  }
}
