import { Component } from 'react';
import isEqual from 'shallowequal';

export default { create };

function create(asyncFn) {
  let displayName = asyncFn.name || asyncFn.displayName || 'Anonymous';
  return class extends Coroutine {
    static get displayName() {
      return `Coroutine(${displayName})`;
    }

    observe(props) {
      return asyncFn(props);
    }
  };
}

class Coroutine extends Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
    this.iterator = null;
    this.isComponentMounted = false;
  }

  observe() {
    throw new Error('Coroutine::observe should be implemented by a subclass');
  }

  forceUpdate(props) {
    const asyncBody = this.observe(props);

    this.iterator = asyncBody;

    const updater = data => {
      if (this.isComponentMounted && this.iterator === asyncBody) {
        this.setState({ data });
      }
    };

    if (isPromiseLike(asyncBody)) {
      asyncBody.then(updater);
    } else {
      const step = this.iterator.next();

      if (isPromiseLike(step)) {
        resolveAsyncIterator(this.iterator, step, updater);
      } else {
        resolveSyncIterator(this.iterator, step, updater);
      }
    }
  }

  componentDidMount() {
    this.isComponentMounted = true;
    return this.forceUpdate(this.props);
  }

  componentWillReceiveProps(nextProps) {
    if (!isEqual(this.props, nextProps)) {
      if (this.iterator && this.iterator.return) {
        this.iterator.return();
      }

      if (this.isComponentMounted) {
        this.forceUpdate(nextProps);
      }
    }
  }

  componentWillUnmount() {
    if (this.iterator && this.iterator.return) {
      this.iterator.return();
    }

    this.isComponentMounted = false;
  }

  render() {
    return this.state.data;
  }
}

function resolveSyncIterator(i, step, cb) {
  if (!step.done) {
    if (isPromiseLike(step.value)) {
      step.value.then(data => resolveSyncIterator(i, i.next(data), cb));
    } else {
      resolveSyncIterator(i, i.next(step.value), cb);
    }
  } else {
    cb(step.value);
  }
}

function resolveAsyncIterator(i, step, cb) {
  step.then(data => {
    if (data.value !== undefined) {
      cb(data.value);
    }

    return !data.done && resolveAsyncIterator(i, i.next(), cb);
  });
}

function isPromiseLike(target) {
  return target && typeof target.then === 'function';
}

import { Component } from 'react';
import isEqual from 'shallowequal';

export default { create };

function create(asyncFn) {
  let displayName = asyncFn.name || asyncFn.displayName || 'Anonymous';
  return class extends Coroutine {
    static get displayName() {
      return `Coroutine(${displayName})`;
    }

    observe(props) {
      return asyncFn(props);
    }
  };
}

class Coroutine extends Component {
  constructor(props) {
    super(props);
    this.state = { data: null };
    this.iterator = null;
    this.isComponentMounted = false;
  }

  observe() {
    throw new Error('Coroutine::observe should be implemented by a subclass');
  }

  forceUpdate(props) {
    const asyncBody = this.observe(props);

    this.iterator = asyncBody;

    const updater = data => {
      if (this.isComponentMounted && this.iterator === asyncBody) {
        this.setState({ data });
      }
    };

    if (isPromiseLike(asyncBody)) {
      // asyncFn is Async Function, awaiting for the final result
      asyncBody.then(updater);
    } else {
      const step = this.iterator.next();

      if (isPromiseLike(step)) {
        // asyncFn is Async Generator, rendering every time it yields
        resolveAsyncIterator(this.iterator, step, updater);
      } else {
        // asyncFn is Sync Generator, rendering the final result, awaiting yielded promises
        resolveSyncIterator(this.iterator, step, updater);
      }
    }
  }

  componentDidMount() {
    this.isComponentMounted = true;
    return this.forceUpdate(this.props);
  }

  componentWillReceiveProps(nextProps) {
    if (!isEqual(this.props, nextProps)) {
      if (this.iterator && this.iterator.return) {
        this.iterator.return();
      }

      if (this.isComponentMounted) {
        this.forceUpdate(nextProps);
      }
    }
  }

  componentWillUnmount() {
    if (this.iterator && this.iterator.return) {
      this.iterator.return();
    }

    this.isComponentMounted = false;
  }

  render() {
    return this.state.data;
  }
}

function resolveSyncIterator(i, step, cb) {
  if (!step.done) {
    if (isPromiseLike(step.value)) {
      step.value.then(data => resolveSyncIterator(i, i.next(data), cb));
    } else {
      resolveSyncIterator(i, i.next(step.value), cb);
    }
  } else {
    cb(step.value);
  }
}

function resolveAsyncIterator(i, step, cb) {
  step.then(data => {
    if (data.value !== undefined) {
      cb(data.value);
    }

    return !data.done && resolveAsyncIterator(i, i.next(), cb);
  });
}

function isPromiseLike(target) {
  return target && typeof target.then === 'function';
}

add generic comments to the core algorithm