import { Dimensions, I18nManager } from 'react-native';
import getSceneIndicesForInterpolationInputRange from '../../utils/getSceneIndicesForInterpolationInputRange';
import { Scene, SceneInterpolatorProps } from '../../types';

function hasHeader(scene: Scene) {
  if (!scene) {
    return true;
  }
  const { descriptor } = scene;
  return descriptor.options.header !== null;
}

const crossFadeInterpolation = (
  scenes: Scene[],
  first: number,
  index: number,
  last: number
): { inputRange: number[]; outputRange: number[]; extrapolate: 'clamp' } => ({
  inputRange: [
    first,
    first + 0.001,
    index - 0.9,
    index - 0.2,
    index,
    last - 0.001,
    last,
  ],
  outputRange: [
    0,
    hasHeader(scenes[first]) ? 0 : 1,
    hasHeader(scenes[first]) ? 0 : 1,
    hasHeader(scenes[first]) ? 0.3 : 1,
    hasHeader(scenes[index]) ? 1 : 0,
    hasHeader(scenes[last]) ? 0 : 1,
    0,
  ],
  extrapolate: 'clamp',
});

/**
 * Utilities that build the style for the navigation header.
 *
 * +-------------+-------------+-------------+
 * |             |             |             |
 * |    Left     |   Title     |   Right     |
 * |  Component  |  Component  | Component   |
 * |             |             |             |
 * +-------------+-------------+-------------+
 */

function isGoingBack(scenes: Scene[]) {
  const lastSceneIndexInScenes = scenes.length - 1;
  return !scenes[lastSceneIndexInScenes].isActive;
}

function forLayout(props: SceneInterpolatorProps) {
  const { layout, position, scene, scenes, mode } = props;
  if (mode !== 'float') {
    return {};
  }
  const isBack = isGoingBack(scenes);

  const interpolate = getSceneIndicesForInterpolationInputRange(props);
  if (!interpolate) return {};

  const { first, last } = interpolate;
  const index = scene.index;

  // We really shouldn't render the scene at all until we know the width of the
  // stack. That said, in every case that I have ever seen, this has just been
  // the full width of the window. This won't continue to be true if we support
  // layouts like iPad master-detail. For now, in order to solve
  // https://github.com/react-navigation/react-navigation/issues/4264, I have
  // opted for the heuristic that we will use the window width until we have
  // measured (and they will usually be the same).
  const width = layout.initWidth || Dimensions.get('window').width;

  // Make sure the header stays hidden when transitioning between 2 screens
  // with no header.
  if (
    (isBack && !hasHeader(scenes[index]) && !hasHeader(scenes[last])) ||
    (!isBack && !hasHeader(scenes[first]) && !hasHeader(scenes[index]))
  ) {
    return {
      transform: [{ translateX: width }],
    };
  }

  const rtlMult = I18nManager.isRTL ? -1 : 1;
  const translateX = position.interpolate({
    inputRange: [first, index, last],
    outputRange: [
      rtlMult * (hasHeader(scenes[first]) ? 0 : width),
      rtlMult * (hasHeader(scenes[index]) ? 0 : isBack ? width : -width),
      rtlMult * (hasHeader(scenes[last]) ? 0 : -width),
    ],
    extrapolate: 'clamp',
  });

  return {
    transform: [{ translateX }],
  };
}

function forLeft(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };

  const { first, last } = interpolate;
  const index = scene.index;

  return {
    opacity: position.interpolate(
      crossFadeInterpolation(scenes, first, index, last)
    ),
  };
}

function forCenter(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };

  const { first, last } = interpolate;
  const index = scene.index;

  return {
    opacity: position.interpolate(
      crossFadeInterpolation(scenes, first, index, last)
    ),
  };
}

function forRight(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };
  const { first, last } = interpolate;
  const index = scene.index;

  return {
    opacity: position.interpolate(
      crossFadeInterpolation(scenes, first, index, last)
    ),
  };
}

/**
 * iOS UINavigationController style interpolators
 */

function forLeftButton(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };

  const { first, last } = interpolate;
  const index = scene.index;

  // The gist of what we're doing here is animating the left button _normally_ (fast fade)
  // when both scenes in transition have headers. When the current, next, or previous scene _don't_
  // have a header, we don't fade the button, and only set it's opacity to 0 at the last moment
  // of the transition.
  const inputRange = [
    first,
    first + 0.001,
    first + Math.abs(index - first) / 2,
    index,
    last - Math.abs(last - index) / 2,
    last - 0.001,
    last,
  ];
  const outputRange = [
    0,
    hasHeader(scenes[first]) ? 0 : 1,
    hasHeader(scenes[first]) ? 0.3 : 1,
    hasHeader(scenes[index]) ? 1 : 0,
    hasHeader(scenes[last]) ? 0.3 : 1,
    hasHeader(scenes[last]) ? 0 : 1,
    0,
  ];

  return {
    opacity: position.interpolate({
      inputRange,
      outputRange,
      extrapolate: 'clamp',
    }),
  };
}

/*
 * NOTE: this offset calculation is an approximation that gives us
 * decent results in many cases, but it is ultimately a poor substitute
 * for text measurement. See the comment on title for more information.
 *
 * - 70 is the width of the left button area.
 * - 25 is the width of the left button icon (to account for label offset)
 */
const LEFT_LABEL_OFFSET = Dimensions.get('window').width / 2 - 70 - 25;

function forLeftLabel(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };

  const { first, last } = interpolate;
  const index = scene.index;

  const offset = LEFT_LABEL_OFFSET;

  // Similarly to the animation of the left label, when animating to or from a scene without
  // a header, we keep the label at full opacity and in the same position for as long as possible.
  return {
    // For now we fade out the label before fading in the title, so the
    // differences between the label and title position can be hopefully not so
    // noticable to the user
    opacity: position.interpolate({
      inputRange: [
        first,
        first + 0.001,
        index - 0.35,
        index,
        index + 0.5,
        last - 0.001,
        last,
      ],
      outputRange: [
        0,
        hasHeader(scenes[first]) ? 0 : 1,
        hasHeader(scenes[first]) ? 0 : 1,
        hasHeader(scenes[index]) ? 1 : 0,
        hasHeader(scenes[last]) ? 0.5 : 1,
        hasHeader(scenes[last]) ? 0 : 1,
        0,
      ],
      extrapolate: 'clamp',
    }),
    transform: [
      {
        translateX: position.interpolate({
          inputRange: [first, first + 0.001, index, last - 0.001, last],
          outputRange: I18nManager.isRTL
            ? [
                -offset * 1.5,
                hasHeader(scenes[first]) ? -offset * 1.5 : 0,
                0,
                hasHeader(scenes[last]) ? offset : 0,
                offset,
              ]
            : [
                offset,
                hasHeader(scenes[first]) ? offset : 0,
                0,
                hasHeader(scenes[last]) ? -offset * 1.5 : 0,
                -offset * 1.5,
              ],
          extrapolate: 'clamp',
        }),
      },
    ],
  };
}

/*
 * NOTE: this offset calculation is a an approximation that gives us
 * decent results in many cases, but it is ultimately a poor substitute
 * for text measurement. We want the back button label to transition
 * smoothly into the title text and to do this we need to understand
 * where the title is positioned within the title container (since it is
 * centered).
 *
 * - 70 is the width of the left button area.
 * - 25 is the width of the left button icon (to account for label offset)
 */
const TITLE_OFFSET_IOS = Dimensions.get('window').width / 2 - 70 + 25;

function forCenterFromLeft(props: SceneInterpolatorProps) {
  const { position, scene, scenes } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);

  if (!interpolate) return { opacity: 0 };

  const { first, last } = interpolate;
  const index = scene.index;
  const offset = TITLE_OFFSET_IOS;

  return {
    opacity: position.interpolate({
      inputRange: [
        first,
        first + 0.001,
        index - 0.5,
        index,
        index + 0.7,
        last - 0.001,
        last,
      ],
      outputRange: [
        0,
        hasHeader(scenes[first]) ? 0 : 1,
        hasHeader(scenes[first]) ? 0 : 1,
        hasHeader(scenes[index]) ? 1 : 0,
        hasHeader(scenes[last]) ? 0 : 1,
        hasHeader(scenes[last]) ? 0 : 1,
        0,
      ],
      extrapolate: 'clamp',
    }),
    transform: [
      {
        translateX: position.interpolate({
          inputRange: [first, first + 0.001, index, last - 0.001, last],
          outputRange: I18nManager.isRTL
            ? [
                -offset,
                hasHeader(scenes[first]) ? -offset : 0,
                0,
                hasHeader(scenes[last]) ? offset : 0,
                offset,
              ]
            : [
                offset,
                hasHeader(scenes[first]) ? offset : 0,
                0,
                hasHeader(scenes[last]) ? -offset : 0,
                -offset,
              ],
          extrapolate: 'clamp',
        }),
      },
    ],
  };
}

// Fade in background of header while transitioning
function forBackgroundWithFade(props: SceneInterpolatorProps) {
  const { position, scene } = props;
  const sceneRange = getSceneIndicesForInterpolationInputRange(props);
  if (!sceneRange) return { opacity: 0 };
  return {
    opacity: position.interpolate({
      inputRange: [sceneRange.first, scene.index, sceneRange.last],
      outputRange: [0, 1, 0],
      extrapolate: 'clamp',
    }),
  };
}

const VISIBLE = { opacity: 1 };
const HIDDEN = { opacity: 0 };

// Toggle visibility of header without fading
function forBackgroundWithInactiveHidden({
  navigation,
  scene,
}: SceneInterpolatorProps) {
  return navigation.state.index === scene.index ? VISIBLE : HIDDEN;
}

// Translate the background with the card
const BACKGROUND_OFFSET = Dimensions.get('window').width;

function forBackgroundWithTranslation(props: SceneInterpolatorProps) {
  const { position, scene } = props;
  const interpolate = getSceneIndicesForInterpolationInputRange(props);
  if (!interpolate) return { opacity: 0 };
  const { first, last } = interpolate;
  const index = scene.index;
  const offset = BACKGROUND_OFFSET;
  const outputRange = [offset, 0, -offset];
  return {
    transform: [
      {
        translateX: position.interpolate({
          inputRange: [first, index, last],
          outputRange: I18nManager.isRTL ? outputRange.reverse() : outputRange,
          extrapolate: 'clamp',
        }),
      },
    ],
  };
}

// Default to fade transition
const forBackground = forBackgroundWithInactiveHidden;

export default {
  forLayout,
  forLeft,
  forLeftButton,
  forLeftLabel,
  forCenterFromLeft,
  forCenter,
  forRight,
  forBackground,
  forBackgroundWithInactiveHidden,
  forBackgroundWithFade,
  forBackgroundWithTranslation,
};
