/**
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {ReactNodeList} from 'shared/ReactTypes';
import type {ReactNativeCSType} from './ReactNativeCSTypes';

// Provided by CS:
import {CSStatefulComponent} from 'CSStatefulComponent';

import ReactFiberReconciler from 'react-reconciler';
import ReactVersion from 'shared/ReactVersion';

const emptyObject = {};

type Container = {
  pendingChild: null | Instance | TextInstance,
};
type InstanceProps = Props & {children: Array<Instance>};
type Instance = {
  props: InstanceProps,
  options: {key: string, ref: null},
  data: {type: 'NATIVE', name: string},
};
type Props = Object;
type TextInstance = Instance;

// We currently don't actually return a new state. We only use state updaters to trigger a
// rerender. Therefore our state updater is the identity functions. When we later deal
// with sync scheduling and aborted renders, we will need to update the state in render.
const identityUpdater = state => state;
// We currently don't have a hook for aborting render. Will add one once it is in place
// in React Native proper.
const infiniteDeadline = {
  timeRemaining: function() {
    return Infinity;
  },
};

const arePropsEqual = (oldProps: Props, newProps: Props): boolean => {
  var key;
  for (key in newProps) {
    if (key === 'children') {
      // Skip special case.
      continue;
    }
    if (newProps[key] !== oldProps[key]) {
      return false;
    }
  }
  for (key in oldProps) {
    if (key === 'children') {
      // Skip special case.
      continue;
    }
    if (!(key in newProps)) {
      return false;
    }
  }
  return true;
};

// React doesn't expose its full keypath. To manage lifetime of instances, we instead use IDs.
let nextComponentKey = 0;

// Callback. Currently this is global. TODO: This should be per root.
let scheduledCallback = null;
// Updater. This is the CS updater we use to trigger the update. TODO: This should be per root.
let scheduleUpdate = null;

const ReactNativeCSFiberRenderer = ReactFiberReconciler({
  appendInitialChild(
    parentInstance: Instance,
    child: Instance | TextInstance,
  ): void {
    if (parentInstance.props) {
      parentInstance.props.children.push(child);
    } else {
      // CSCustom
      (parentInstance: any).children.push(child);
    }
  },

  createInstance(
    type: string,
    props: Props,
    rootContainerInstance: Container,
    hostContext: {},
    internalInstanceHandle: Object,
  ): Instance {
    let key = '' + nextComponentKey++;
    let ref = null; // TODO: Always create Ref object so that getPublicInstance can use it.
    // We need a new props object so that we can represent flattened children.
    let newProps = Object.assign({}, props);
    newProps.children = [];
    if (type === 'CSCustom') {
      // Special cased type that treats the props as the object.
      // Useful for custom children types like FlexItem.
      return newProps;
    }
    return {
      props: newProps,
      options: {key, ref},
      data: {type: 'NATIVE', name: type},
    };
  },

  createTextInstance(
    text: string,
    rootContainerInstance: Container,
    hostContext: {},
    internalInstanceHandle: Object,
  ): TextInstance {
    // Could auto-translate to CSText with some host context defined attributes.
    throw new Error('Not yet implemented.');
  },

  finalizeInitialChildren(
    parentInstance: Instance,
    type: string,
    props: Props,
    rootContainerInstance: Container,
  ): boolean {
    return false;
  },

  getRootHostContext(): {} {
    return emptyObject;
  },

  getChildHostContext(): {} {
    return emptyObject;
  },

  getPublicInstance(instance: Instance) {
    return instance.options.ref;
  },

  prepareForCommit(): void {},

  prepareUpdate(
    instance: Instance,
    type: string,
    oldProps: Props,
    newProps: Props,
    rootContainerInstance: Container,
    hostContext: {},
  ): null | InstanceProps {
    if (arePropsEqual(oldProps, newProps)) {
      return null;
    }
    return Object.assign({}, newProps);
  },

  resetAfterCommit(): void {},

  shouldDeprioritizeSubtree(type: string, props: Props): boolean {
    return false;
  },

  scheduleDeferredCallback(callback) {
    scheduledCallback = callback;
    if (scheduleUpdate !== null) {
      scheduleUpdate(identityUpdater);
    }
    return 0;
  },

  cancelDeferredCallback() {
    // Noop. This is always called right before scheduling a new update, so
    // should be fine. This renderer won't use requestIdleCallback, anyway.
    // Will switch to use shouldYield() API instead.
  },

  shouldSetTextContent(type: string, props: Props): boolean {
    // TODO: Figure out when we should allow text content.
    return false;
  },

  useSyncScheduling: false,

  now(): number {
    // TODO: Enable expiration by implementing this method.
    return 0;
  },

  persistence: {
    cloneInstance(
      instance: Instance,
      updatePayload: null | InstanceProps,
      type: string,
      oldProps: Props,
      newProps: Props,
      internalInstanceHandle: Object,
      keepChildren: boolean,
      recyclableInstance: null | Instance,
    ): Instance {
      let newInstanceProps = updatePayload;
      if (newInstanceProps === null) {
        newInstanceProps = Object.assign({}, newProps);
      }
      // We need a new props object so that we can represent flattened children.
      newInstanceProps.children = keepChildren ? instance.props.children : [];
      if (type === 'CSCustom') {
        return newInstanceProps;
      }
      return {
        props: newInstanceProps,
        options: instance.options,
        data: instance.data,
      };
    },

    createContainerChildSet(container: Container): Container {
      // We'll only ever have one instance in the container.
      container.pendingChild = null;
      return container;
    },

    appendChildToContainerChildSet(
      childSet: Container,
      child: Instance | TextInstance,
    ): void {
      if (childSet.pendingChild !== null) {
        throw new Error(
          'CSReact does not support top level fragments. Wrap it in a primitve.',
        );
      }
      childSet.pendingChild = child;
    },

    finalizeContainerChildren(
      container: Container,
      newChildren: Container,
    ): void {},

    replaceContainerChildren(
      container: Container,
      newChildren: Container,
    ): void {},
  },
});

ReactNativeCSFiberRenderer.injectIntoDevTools({
  bundleType: __DEV__ ? 1 : 0,
  version: ReactVersion,
  rendererPackageName: 'react-cs-renderer',
});

type ReactCSProps = {children: ReactNodeList};
type ReactCSState = {
  root: Object,
  container: {
    pendingChild: null | Instance | TextInstance,
  },
};

const ReactCS = CSStatefulComponent({
  getInitialState({props}: {props: ReactCSProps}): ReactCSState {
    let container = {
      pendingChild: null,
    };
    let root = ReactNativeCSFiberRenderer.createContainer(container, false);
    return {root, container};
  },
  render({
    props,
    state,
    stateUpdater,
  }: {
    props: ReactCSProps,
    state: ReactCSState,
    stateUpdater: (update: (oldState: ReactCSState) => ReactCSState) => void,
  }) {
    scheduleUpdate = stateUpdater;
    // TODO: For a props rerender updateContainer will schedule an additional state
    // update even though it is not necessary since we're already rendering.
    // We should only call scheduleUpdate for a React setState, not a top level
    // props update.
    ReactNativeCSFiberRenderer.updateContainer(
      props.children,
      state.root,
      null,
      null,
    );
    if (scheduledCallback) {
      const callback = scheduledCallback;
      scheduledCallback = null;
      callback(infiniteDeadline);
    }
    return state.container.pendingChild;
  },
  getInstance({state}: {state: ReactCSState}) {
    return ReactNativeCSFiberRenderer.getPublicRootInstance(state.root);
  },
  // TODO: Unmount hook. E.g. finalizer.
});

export default (ReactCS: ReactNativeCSType);
