import Observable from 'zen-observable-ts';

import {
  GraphQLRequest,
  NextLink,
  Operation,
  RequestHandler,
  FetchResult,
} from './types';

import {
  validateOperation,
  isTerminating,
  LinkError,
  transformOperation,
  createOperation,
} from './linkUtils';

const passthrough = (op, forward) => (forward ? forward(op) : Observable.of());

const toLink = (handler: RequestHandler | ApolloLink) =>
  typeof handler === 'function' ? new ApolloLink(handler) : handler;

export const empty = (): ApolloLink =>
  new ApolloLink((op, forward) => Observable.of());

export const from = (links: ApolloLink[]): ApolloLink => {
  if (links.length === 0) return empty();

  return links.map(toLink).reduce((x, y) => x.concat(y));
};

export const split = (
  test: (op: Operation) => boolean,
  left: ApolloLink | RequestHandler,
  right: ApolloLink | RequestHandler = new ApolloLink(passthrough),
): ApolloLink => {
  const leftLink = toLink(left);
  const rightLink = toLink(right);

  if (isTerminating(leftLink) && isTerminating(rightLink)) {
    return new ApolloLink(operation => {
      return test(operation)
        ? leftLink.request(operation) || Observable.of()
        : rightLink.request(operation) || Observable.of();
    });
  } else {
    return new ApolloLink((operation, forward) => {
      return test(operation)
        ? leftLink.request(operation, forward) || Observable.of()
        : rightLink.request(operation, forward) || Observable.of();
    });
  }
};

// join two Links together
export const concat = (
  first: ApolloLink | RequestHandler,
  second: ApolloLink | RequestHandler,
) => {
  const firstLink = toLink(first);
  if (isTerminating(firstLink)) {
    console.warn(
      new LinkError(
        `You are calling concat on a terminating link, which will have no effect`,
        firstLink,
      ),
    );
    return firstLink;
  }
  const nextLink = toLink(second);

  if (isTerminating(nextLink)) {
    return new ApolloLink(
      operation =>
        firstLink.request(
          operation,
          op => nextLink.request(op) || Observable.of(),
        ) || Observable.of(),
    );
  } else {
    return new ApolloLink((operation, forward) => {
      return (
        firstLink.request(operation, op => {
          return nextLink.request(op, forward) || Observable.of();
        }) || Observable.of()
      );
    });
  }
};

export class ApolloLink {
  constructor(request?: RequestHandler) {
    if (request) this.request = request;
  }

  public static empty = empty;
  public static from = from;
  public static split = split;
  public static execute = execute;

  public split(
    test: (op: Operation) => boolean,
    left: ApolloLink | RequestHandler,
    right: ApolloLink | RequestHandler = new ApolloLink(passthrough),
  ): ApolloLink {
    return this.concat(split(test, left, right));
  }

  public concat(next: ApolloLink | RequestHandler): ApolloLink {
    return concat(this, next);
  }

  public request(
    operation: Operation,
    forward?: NextLink,
  ): Observable<FetchResult> | null {
    throw new Error('request is not implemented');
  }
}

export function execute(
  link: ApolloLink,
  operation: GraphQLRequest,
): Observable<FetchResult> {
  return (
    link.request(
      createOperation(
        operation.context,
        transformOperation(validateOperation(operation)),
      ),
    ) || Observable.of()
  );
}