import ReactElement from 'react/lib/ReactElement';
import ReactCurrentOwner from 'react/lib/ReactCurrentOwner';

import invariant from 'fbjs/lib/invariant';
import emptyObject from 'fbjs/lib/emptyObject';
import warning from 'fbjs/lib/warning';

import ReactCompositeComponent from 'react-dom/lib/ReactCompositeComponent';
import ReactInstanceMap from 'react-dom/lib/ReactInstanceMap';
import ReactInstrumentation from 'react-dom/lib/ReactInstrumentation';

import removeDevTool from './utils/removeDevTool';

class ReactCompositeComponentMixinImpl {
}

ReactCompositeComponentMixinImpl.prototype = {
  ...ReactCompositeComponentMixinImpl.prototype,
  ...ReactCompositeComponent,
};

function warnIfInvalidElement(Component, element) {
  if (process.env.NODE_ENV !== 'production') {
    warning(
      element === null || element === false || ReactElement.isValidElement(element),
      '%s(...): A valid React element (or null) must be returned. You may have ' +
      'returned undefined, an array or some other invalid object.',
      Component.displayName || Component.name || 'Component'
    );
  }
}

const CompositeTypes = {
  ImpureClass: 0,
  PureClass: 1,
  StatelessFunctional: 2,
};

function shouldConstruct(Component) {
  return (!!Component.prototype && Component.prototype.isReactComponent);
}

function isPureComponent(Component) {
  return !!(Component.prototype && Component.prototype.isPureReactComponent);
}

let invokeComponentDidMountWithTimer;

if (process.env.NODE_ENV !== 'production') {
  invokeComponentDidMountWithTimer = function _invokeComponentDidMountWithTimer() {
    const publicInstance = this._instance;
    if (this._debugID !== 0) {
      ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
        this._debugID,
        'componentDidMount'
      );
    }
    publicInstance.componentDidMount();
    if (this._debugID !== 0) {
      ReactInstrumentation.debugTool.onEndLifeCycleTimer(
        this._debugID,
        'componentDidMount'
      );
    }
  };
}

class StatelessComponent {
  render() {
    const componentCreator = ReactInstanceMap.get(this)._currentElement.type;
    const element = componentCreator(this.props, this.context, this.updater);
    warnIfInvalidElement(componentCreator, element);
    return element;
  }
}

class React3CompositeComponentWrapper extends ReactCompositeComponentMixinImpl {
  constructor(element, react3RendererInstance) {
    super();
    this._react3RendererInstance = react3RendererInstance;

    this.construct(element);
  }

  getHostMarkup() {
    return super.getHostNode();
  }

  construct(element) {
    super.construct(element);

    this._threeObject = null;
  }


  /**
   * @see ReactCompositeComponent.
   *
   * Cloned because it needs to set _threeObject and remove dev tool
   *
   * Call the component's `render` method and update the DOM accordingly.
   *
   * @param {ReactReconcileTransaction} transaction
   * @param context
   * @internal
   */
  _updateRenderedComponent(transaction, context) {
    let devToolRemoved;
    if (process.env.NODE_ENV !== 'production') {
      devToolRemoved = removeDevTool();
    }

    super._updateRenderedComponent(transaction, context);

    if (process.env.NODE_ENV !== 'production') {
      if (devToolRemoved) {
        removeDevTool.restore();
      }
    }

    this._threeObject = this._renderedComponent._threeObject;
  }

  _instantiateReactComponent(element, shouldHaveDebugID) {
    return this._react3RendererInstance.instantiateReactComponent(element, shouldHaveDebugID);
  }


  // TODO: prevInstance
  _replaceNodeWithMarkup(oldMarkup, nextMarkup) {
    const parentMarkup = oldMarkup.parentMarkup;

    const ownerChildrenMarkups = parentMarkup.childrenMarkup;

    const indexInParent = ownerChildrenMarkups.indexOf(oldMarkup);

    if (process.env.NODE_ENV !== 'production') {
      invariant(indexInParent !== -1, 'The node has no parent');
    } else {
      invariant(indexInParent !== -1);
    }

    const parentInternalComponent = parentMarkup.threeObject.userData.react3internalComponent;
    const originalInternalComponent = oldMarkup.threeObject.userData.react3internalComponent;

    parentInternalComponent.removeChild(originalInternalComponent, oldMarkup);
    const nextChild = nextMarkup.threeObject.userData.react3internalComponent;
    nextChild._mountIndex = indexInParent;
    parentInternalComponent.createChild(nextChild, null, nextMarkup);
  }

  // See ReactCompositeComponent.mountComponent

  /**
   * Initializes the component, renders markup, and registers event listeners.
   *
   * @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
   * @param {?object} hostParent
   * @param {?object} hostContainerInfo
   * @param {?object} context
   * @return {?string} Rendered markup to be inserted into the DOM.
   * @final
   * @internal
   */
  mountComponent(transaction,
    hostParent,
    hostContainerInfo,
    context) {
    this._context = context;
    this._mountOrder = this._react3RendererInstance.nextMountID++;
    this._hostParent = hostParent;
    this._hostContainerInfo = hostContainerInfo;

    const publicProps = this._currentElement.props;
    const publicContext = this._processContext(context);

    const Component = this._currentElement.type;

    const updateQueue = transaction.getUpdateQueue();

    // Initialize the public class
    const doConstruct = shouldConstruct(Component);
    let inst = this._constructComponent(
      doConstruct,
      publicProps,
      publicContext,
      updateQueue
    );

    let renderedElement;

    // Support functional components
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      warnIfInvalidElement(Component, renderedElement);
      invariant(
        inst === null ||
        inst === false ||
        ReactElement.isValidElement(inst),
        '%s(...): A valid React element (or null) must be returned. You may have ' +
        'returned undefined, an array or some other invalid object.',
        Component.displayName || Component.name || 'Component'
      );
      inst = new StatelessComponent(Component);
      this._compositeType = CompositeTypes.StatelessFunctional;
    } else if (isPureComponent(Component)) {
      this._compositeType = CompositeTypes.PureClass;
    } else {
      this._compositeType = CompositeTypes.ImpureClass;
    }

    if (process.env.NODE_ENV !== 'production') {
      // This will throw later in _renderValidatedComponent, but add an early
      // warning now to help debugging
      if (!inst.render) {
        warning(
          false,
          '%s(...): No `render` method found on the returned component ' +
          'instance: you may have forgotten to define `render`.',
          Component.displayName || Component.name || 'Component'
        );
      }

      const propsMutated = inst.props !== publicProps;
      const componentName =
        Component.displayName || Component.name || 'Component';

      warning(
        inst.props === undefined || !propsMutated,
        '%s(...): When calling super() in `%s`, make sure to pass ' +
        'up the same props that your component\'s constructor was passed.',
        componentName, componentName
      );
    }

    // These should be set up in the constructor, but as a convenience for
    // simpler class abstractions, we set them up after the fact.
    inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;

    this._instance = inst;

    // Store a reference from the instance back to the internal representation
    ReactInstanceMap.set(inst, this);

    if (process.env.NODE_ENV !== 'production') {
      // Since plain JS classes are defined without any special initialization
      // logic, we can not catch common errors early. Therefore, we have to
      // catch them here, at initialization time, instead.
      warning(
        !inst.getInitialState ||
        inst.getInitialState.isReactClassApproved,
        'getInitialState was defined on %s, a plain JavaScript class. ' +
        'This is only supported for classes created using React.createClass. ' +
        'Did you mean to define a state property instead?',
        this.getName() || 'a component'
      );
      warning(
        !inst.getDefaultProps ||
        inst.getDefaultProps.isReactClassApproved,
        'getDefaultProps was defined on %s, a plain JavaScript class. ' +
        'This is only supported for classes created using React.createClass. ' +
        'Use a static property to define defaultProps instead.',
        this.getName() || 'a component'
      );
      warning(
        !inst.propTypes,
        'propTypes was defined as an instance property on %s. Use a static ' +
        'property to define propTypes instead.',
        this.getName() || 'a component'
      );
      warning(
        !inst.contextTypes,
        'contextTypes was defined as an instance property on %s. Use a ' +
        'static property to define contextTypes instead.',
        this.getName() || 'a component'
      );
      warning(
        typeof inst.componentShouldUpdate !== 'function',
        '%s has a method called ' +
        'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' +
        'The name is phrased as a question because the function is ' +
        'expected to return a value.',
        (this.getName() || 'A component')
      );
      warning(
        typeof inst.componentDidUnmount !== 'function',
        '%s has a method called ' +
        'componentDidUnmount(). But there is no such lifecycle method. ' +
        'Did you mean componentWillUnmount()?',
        this.getName() || 'A component'
      );
      warning(
        typeof inst.componentWillRecieveProps !== 'function',
        '%s has a method called ' +
        'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?',
        (this.getName() || 'A component')
      );
    }
    let initialState = inst.state;
    if (initialState === undefined) {
      inst.state = initialState = null;
    }
    if (!(typeof initialState === 'object' && !Array.isArray(initialState))) {
      if (process.env.NODE_ENV !== 'production') {
        invariant(false,
          '%s.state: must be set to an object or null',
          this.getName() || 'ReactCompositeComponent');
      } else {
        invariant(false);
      }
    }

    this._pendingStateQueue = null;
    this._pendingReplaceState = false;
    this._pendingForceUpdate = false;

    const markup = this.performInitialMount(
      renderedElement,
      hostParent,
      hostContainerInfo,
      transaction,
      context);

    if (inst.componentDidMount) {
      if (process.env.NODE_ENV !== 'production') {
        transaction.getReactMountReady().enqueue(invokeComponentDidMountWithTimer, this);
      } else {
        transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
      }
    }

    return markup;
  }

  _constructComponent(doConstruct,
    publicProps,
    publicContext,
    updateQueue) {
    if (process.env.NODE_ENV !== 'production') {
      ReactCurrentOwner.current = this;
      try {
        return this._constructComponentWithoutOwner(
          doConstruct,
          publicProps,
          publicContext,
          updateQueue
        );
      } finally {
        ReactCurrentOwner.current = null;
      }
    } else {
      return this._constructComponentWithoutOwner(
        doConstruct,
        publicProps,
        publicContext,
        updateQueue
      );
    }
  }

  _constructComponentWithoutOwner(doConstruct,
    publicProps,
    publicContext,
    updateQueue) {
    const Component = this._currentElement.type;
    let instanceOrElement;
    if (doConstruct) {
      if (process.env.NODE_ENV !== 'production') {
        if (this._debugID !== 0) {
          ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
            this._debugID,
            'ctor'
          );
        }
      }
      instanceOrElement = new Component(publicProps, publicContext, updateQueue);
      if (process.env.NODE_ENV !== 'production') {
        if (this._debugID !== 0) {
          ReactInstrumentation.debugTool.onEndLifeCycleTimer(
            this._debugID,
            'ctor'
          );
        }
      }
    } else {
      // This can still be an instance in case of factory components
      // but we'll count this as time spent rendering as the more common case.
      if (process.env.NODE_ENV !== 'production') {
        if (this._debugID !== 0) {
          ReactInstrumentation.debugTool.onBeginLifeCycleTimer(
            this._debugID,
            'render'
          );
        }
      }

      /* eslint-disable new-cap */
      instanceOrElement = Component(publicProps, publicContext, updateQueue);
      /* eslint-enable */

      if (process.env.NODE_ENV !== 'production') {
        if (this._debugID !== 0) {
          ReactInstrumentation.debugTool.onEndLifeCycleTimer(
            this._debugID,
            'render'
          );
        }
      }
    }
    return instanceOrElement;
  }

  /**
   * Needs to be overwritten because emptyObject points to another...
   *
   * Lazily allocates the refs object and stores `component` as `ref`.
   *
   * @param {string} ref Reference name.
   * @param {*} component Component to store as `ref`.
   * @final
   * @private
   */
  attachRef(ref, component) {
    const inst = this.getPublicInstance();
    const refs = inst.refs === emptyObject ? inst.refs = {} : inst.refs;
    refs[ref] = component.getPublicInstance();
  }
}

module.exports = React3CompositeComponentWrapper;
