/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule NativeMethodsMixin
 * @flow
 */
'use strict';

var ReactNative = require('ReactNative');
var ReactNativeFeatureFlags = require('ReactNativeFeatureFlags');
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
var TextInputState = require('TextInputState');
var UIManager = require('UIManager');

var invariant = require('fbjs/lib/invariant');
var findNodeHandle = require('findNodeHandle');

var {
  mountSafeCallback,
  throwOnStylesProp,
  warnForStyleProps,
} = require('NativeMethodsMixinUtils');

import type {
  MeasureInWindowOnSuccessCallback,
  MeasureLayoutOnSuccessCallback,
  MeasureOnSuccessCallback,
} from 'NativeMethodsMixinUtils';
import type {
  ReactNativeBaseComponentViewConfig,
} from 'ReactNativeViewConfigRegistry';

/**
 * `NativeMethodsMixin` provides methods to access the underlying native
 * component directly. This can be useful in cases when you want to focus
 * a view or measure its on-screen dimensions, for example.
 *
 * The methods described here are available on most of the default components
 * provided by React Native. Note, however, that they are *not* available on
 * composite components that aren't directly backed by a native view. This will
 * generally include most components that you define in your own app. For more
 * information, see [Direct
 * Manipulation](docs/direct-manipulation.html).
 */
// TODO (bvaughn) Figure out how to use the NativeMethodsInterface type to-
// ensure that these mixins and ReactNativeFiberHostComponent stay in sync.
// Unfortunately, using it causes Flow to complain WRT createClass mixins:
// "call of method `createClass`. Expected an exact object instead of ..."
var NativeMethodsMixin = {
  /**
   * Determines the location on screen, width, and height of the given view and
   * returns the values via an async callback. If successful, the callback will
   * be called with the following arguments:
   *
   *  - x
   *  - y
   *  - width
   *  - height
   *  - pageX
   *  - pageY
   *
   * Note that these measurements are not available until after the rendering
   * has been completed in native. If you need the measurements as soon as
   * possible, consider using the [`onLayout`
   * prop](docs/view.html#onlayout) instead.
   */
  measure: function(callback: MeasureOnSuccessCallback) {
    UIManager.measure(
      ReactNative.findNodeHandle(this),
      mountSafeCallback(this, callback),
    );
  },

  /**
   * Determines the location of the given view in the window and returns the
   * values via an async callback. If the React root view is embedded in
   * another native view, this will give you the absolute coordinates. If
   * successful, the callback will be called with the following
   * arguments:
   *
   *  - x
   *  - y
   *  - width
   *  - height
   *
   * Note that these measurements are not available until after the rendering
   * has been completed in native.
   */
  measureInWindow: function(callback: MeasureInWindowOnSuccessCallback) {
    UIManager.measureInWindow(
      ReactNative.findNodeHandle(this),
      mountSafeCallback(this, callback),
    );
  },

  /**
   * Like [`measure()`](#measure), but measures the view relative an ancestor,
   * specified as `relativeToNativeNode`. This means that the returned x, y
   * are relative to the origin x, y of the ancestor view.
   *
   * As always, to obtain a native node handle for a component, you can use
   * `ReactNative.findNodeHandle(component)`.
   */
  measureLayout: function(
    relativeToNativeNode: number,
    onSuccess: MeasureLayoutOnSuccessCallback,
    onFail: () => void /* currently unused */,
  ) {
    UIManager.measureLayout(
      ReactNative.findNodeHandle(this),
      relativeToNativeNode,
      mountSafeCallback(this, onFail),
      mountSafeCallback(this, onSuccess),
    );
  },

  /**
   * This function sends props straight to native. They will not participate in
   * future diff process - this means that if you do not include them in the
   * next render, they will remain active (see [Direct
   * Manipulation](docs/direct-manipulation.html)).
   */
  setNativeProps: function(nativeProps: Object) {
    // Ensure ReactNative factory function has configured findNodeHandle.
    // Requiring it won't execute the factory function until first referenced.
    // It's possible for tests that use ReactTestRenderer to reach this point,
    // Without having executed ReactNative.
    // Defer the factory function until now to avoid a cycle with UIManager.
    // TODO (bvaughn) Remove this once ReactNativeStack is dropped.
    require('ReactNative');

    injectedSetNativeProps(this, nativeProps);
  },

  /**
   * Requests focus for the given input or view. The exact behavior triggered
   * will depend on the platform and type of view.
   */
  focus: function() {
    TextInputState.focusTextInput(ReactNative.findNodeHandle(this));
  },

  /**
   * Removes focus from an input or view. This is the opposite of `focus()`.
   */
  blur: function() {
    TextInputState.blurTextInput(ReactNative.findNodeHandle(this));
  },
};

// TODO (bvaughn) Inline this once ReactNativeStack is dropped.
function setNativePropsFiber(componentOrHandle: any, nativeProps: Object) {
  // Class components don't have viewConfig -> validateAttributes.
  // Nor does it make sense to set native props on a non-native component.
  // Instead, find the nearest host component and set props on it.
  // Use findNodeHandle() rather than ReactNative.findNodeHandle() because
  // We want the instance/wrapper (not the native tag).
  let maybeInstance;

  // Fiber errors if findNodeHandle is called for an umounted component.
  // Tests using ReactTestRenderer will trigger this case indirectly.
  // Mimicking stack behavior, we should silently ignore this case.
  // TODO Fix ReactTestRenderer so we can remove this try/catch.
  try {
    maybeInstance = findNodeHandle(componentOrHandle);
  } catch (error) {}

  // If there is no host component beneath this we should fail silently.
  // This is not an error; it could mean a class component rendered null.
  if (maybeInstance == null) {
    return;
  }

  const viewConfig: ReactNativeBaseComponentViewConfig = maybeInstance.viewConfig;

  if (__DEV__) {
    warnForStyleProps(nativeProps, viewConfig.validAttributes);
  }

  var updatePayload = ReactNativeAttributePayload.create(
    nativeProps,
    viewConfig.validAttributes,
  );

  UIManager.updateView(
    maybeInstance._nativeTag,
    viewConfig.uiViewClassName,
    updatePayload,
  );
}

// TODO (bvaughn) Remove this once ReactNativeStack is dropped.
function setNativePropsStack(componentOrHandle: any, nativeProps: Object) {
  // Class components don't have viewConfig -> validateAttributes.
  // Nor does it make sense to set native props on a non-native component.
  // Instead, find the nearest host component and set props on it.
  // Use findNodeHandle() rather than ReactNative.findNodeHandle() because
  // We want the instance/wrapper (not the native tag).
  let maybeInstance = findNodeHandle(componentOrHandle);

  // If there is no host component beneath this we should fail silently.
  // This is not an error; it could mean a class component rendered null.
  if (maybeInstance == null) {
    return;
  }

  let viewConfig: ReactNativeBaseComponentViewConfig;
  if (maybeInstance.viewConfig !== undefined) {
    // ReactNativeBaseComponent
    viewConfig = maybeInstance.viewConfig;
  } else if (
    maybeInstance._instance !== undefined &&
    maybeInstance._instance.viewConfig !== undefined
  ) {
    // ReactCompositeComponentWrapper
    // Some instances (eg Text) define their own viewConfig
    viewConfig = maybeInstance._instance.viewConfig;
  } else {
    // ReactCompositeComponentWrapper
    // Other instances (eg TextInput) defer to their children's viewConfig
    while (maybeInstance._renderedComponent !== undefined) {
      maybeInstance = maybeInstance._renderedComponent;
    }
    viewConfig = maybeInstance.viewConfig;
  }

  const tag: number = typeof maybeInstance.getHostNode === 'function'
    ? maybeInstance.getHostNode()
    : maybeInstance._rootNodeID;

  if (__DEV__) {
    warnForStyleProps(nativeProps, viewConfig.validAttributes);
  }

  var updatePayload = ReactNativeAttributePayload.create(
    nativeProps,
    viewConfig.validAttributes,
  );

  UIManager.updateView(tag, viewConfig.uiViewClassName, updatePayload);
}

// Switching based on fiber vs stack to avoid a lot of inline checks at runtime.
// HACK Normally this injection would be done by the renderer, but in this case
// that would result in a cycle between ReactNative and NativeMethodsMixin.
// We avoid requiring additional code for this injection so it's probably ok?
// TODO (bvaughn) Remove this once ReactNativeStack is gone.
let injectedSetNativeProps: (
  componentOrHandle: any,
  nativeProps: Object,
) => void;
if (ReactNativeFeatureFlags.useFiber) {
  injectedSetNativeProps = setNativePropsFiber;
} else {
  injectedSetNativeProps = setNativePropsStack;
}

if (__DEV__) {
  // hide this from Flow since we can't define these properties outside of
  // __DEV__ without actually implementing them (setting them to undefined
  // isn't allowed by ReactClass)
  var NativeMethodsMixin_DEV = (NativeMethodsMixin: any);
  invariant(
    !NativeMethodsMixin_DEV.componentWillMount &&
      !NativeMethodsMixin_DEV.componentWillReceiveProps,
    'Do not override existing functions.',
  );
  NativeMethodsMixin_DEV.componentWillMount = function() {
    throwOnStylesProp(this, this.props);
  };
  NativeMethodsMixin_DEV.componentWillReceiveProps = function(newProps) {
    throwOnStylesProp(this, newProps);
  };
}

module.exports = NativeMethodsMixin;
