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

'use strict';

const React = require('react');
const {
  Animated,
  LayoutAnimation,
  PanResponder,
  Platform,
  StyleSheet,
  UIManager,
  View,
} = require('react-native');

const AnExSet = require('./AnExSet');

const CIRCLE_SIZE = 80;
const CIRCLE_MARGIN = 18;
const NUM_CIRCLES = 30;

class Circle extends React.Component<any, any> {
  longTimer: number;

  _onLongPress: () => void;
  _toggleIsActive: () => void;
  constructor(props: Object): void {
    super();
    this._onLongPress = this._onLongPress.bind(this);
    this._toggleIsActive = this._toggleIsActive.bind(this);
    this.state = {
      isActive: false,
      pan: new Animated.ValueXY(), // Vectors reduce boilerplate.  (step1: uncomment)
      pop: new Animated.Value(0), // Initial value.               (step2a: uncomment)
    };

    if (Platform.OS === 'android') {
      UIManager.setLayoutAnimationEnabledExperimental(true);
    }
  }

  _onLongPress(): void {
    const config = {tension: 40, friction: 3};
    this.state.pan.addListener(value => {
      // Async listener for state changes  (step1: uncomment)
      this.props.onMove && this.props.onMove(value);
    });
    Animated.spring(this.state.pop, {
      //  Pop to larger size.                      (step2b: uncomment)
      toValue: 1,

      //  Reuse config for convenient consistency  (step2b: uncomment)
      ...config,

      useNativeDriver: false,
    }).start();
    this.setState(
      {
        panResponder: PanResponder.create({
          onPanResponderMove: Animated.event(
            [
              null, // native event - ignore      (step1: uncomment)
              {dx: this.state.pan.x, dy: this.state.pan.y}, // links pan to gestureState  (step1: uncomment)
            ],
            {useNativeDriver: false},
          ),
          onPanResponderRelease: (e, gestureState) => {
            LayoutAnimation.easeInEaseOut(); // @flowfixme animates layout update as one batch (step3: uncomment)
            Animated.spring(this.state.pop, {
              // Pop back to 0                       (step2c: uncomment)
              toValue: 0,

              ...config,
              useNativeDriver: false,
            }).start();
            this.setState({panResponder: undefined});
            this.props.onMove &&
              this.props.onMove({
                x: gestureState.dx + this.props.restLayout.x,
                y: gestureState.dy + this.props.restLayout.y,
              });
            this.props.onDeactivate();
            this.state.pan.removeAllListeners();
          },
        }),
      },
      () => {
        this.props.onActivate();
      },
    );
  }

  render(): React.Node {
    let handlers;
    let dragStyle = null;
    if (this.state.panResponder) {
      handlers = this.state.panResponder.panHandlers;
      dragStyle = {
        //  Used to position while dragging
        position: 'absolute', //  Hoist out of layout                    (step1: uncomment)
        ...this.state.pan.getLayout(), //  Convenience converter                  (step1: uncomment)
      };
    } else {
      handlers = {
        onStartShouldSetResponder: () => !this.state.isActive,
        onResponderGrant: () => {
          this.state.pan.setValue({x: 0, y: 0}); // reset                (step1: uncomment)
          this.state.pan.setOffset(this.props.restLayout); // offset from onLayout (step1: uncomment)
          /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses
           * an error found when Flow v0.63 was deployed. To see the error
           * delete this comment and run Flow. */
          this.longTimer = setTimeout(this._onLongPress, 300);
        },
        onResponderRelease: () => {
          if (!this.state.panResponder) {
            /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment
             * suppresses an error found when Flow v0.63 was deployed. To see
             * the error delete this comment and run Flow. */
            clearTimeout(this.longTimer);
            this._toggleIsActive();
          }
        },
      };
    }
    const animatedStyle: Object = {
      shadowOpacity: this.state.pop, // no need for interpolation            (step2d: uncomment)
      transform: [
        {
          scale: this.state.pop.interpolate({
            inputRange: [0, 1],
            outputRange: [1, 1.3], // scale up from 1 to 1.3               (step2d: uncomment)
          }),
        },
      ],
    };
    const openVal = this.props.openVal;
    let innerOpenStyle = null;
    if (this.props.dummy) {
      animatedStyle.opacity = 0;
    } else if (this.state.isActive) {
      innerOpenStyle = [
        styles.open,
        {
          // (step4: uncomment)
          left: openVal.interpolate({
            inputRange: [0, 1],
            outputRange: [this.props.restLayout.x, 0],
          }),
          top: openVal.interpolate({
            inputRange: [0, 1],
            outputRange: [this.props.restLayout.y, 0],
          }),
          width: openVal.interpolate({
            inputRange: [0, 1],
            outputRange: [CIRCLE_SIZE, this.props.containerLayout.width],
          }),
          height: openVal.interpolate({
            inputRange: [0, 1],
            outputRange: [CIRCLE_SIZE, this.props.containerLayout.height],
          }),
          margin: openVal.interpolate({
            inputRange: [0, 1],
            outputRange: [CIRCLE_MARGIN, 0],
          }),
          borderRadius: openVal.interpolate({
            inputRange: [-0.15, 0, 0.5, 1],
            outputRange: [0, CIRCLE_SIZE / 2, CIRCLE_SIZE * 1.3, 0],
          }),
        },
      ];
    }
    return (
      <Animated.View
        onLayout={this.props.onLayout}
        style={[
          styles.dragView,
          dragStyle,
          animatedStyle,
          this.state.isActive ? styles.open : null,
        ]}
        {...handlers}>
        <Animated.View style={[styles.circle, innerOpenStyle]}>
          <AnExSet
            containerLayout={this.props.containerLayout}
            id={this.props.id}
            isActive={this.state.isActive}
            openVal={this.props.openVal}
            onDismiss={this._toggleIsActive}
          />
        </Animated.View>
      </Animated.View>
    );
  }
  _toggleIsActive(velocity) {
    const config = {tension: 30, friction: 7};
    if (this.state.isActive) {
      Animated.spring(this.props.openVal, {
        toValue: 0,
        ...config,
        useNativeDriver: false,
      }).start(() => {
        // (step4: uncomment)
        this.setState({isActive: false}, this.props.onDeactivate);
      }); // (step4: uncomment)
    } else {
      this.props.onActivate();
      this.setState({isActive: true, panResponder: undefined}, () => {
        // this.props.openVal.setValue(1);                                             // (step4: comment)
        Animated.spring(this.props.openVal, {
          toValue: 1,
          ...config,
          useNativeDriver: false,
        }).start(); // (step4: uncomment)
      });
    }
  }
}

class AnExApp extends React.Component<any, any> {
  _onMove: (position: Point) => void;
  constructor(props: any): void {
    super(props);
    const keys = [];
    for (let idx = 0; idx < NUM_CIRCLES; idx++) {
      keys.push('E' + idx);
    }
    this.state = {
      keys,
      restLayouts: [],
      openVal: new Animated.Value(0),
    };
    this._onMove = this._onMove.bind(this);
  }

  render(): React.Node {
    const circles = this.state.keys.map((key, idx) => {
      if (key === this.state.activeKey) {
        return <Circle key={key + 'd'} dummy={true} />;
      } else {
        let onLayout = null;
        if (!this.state.restLayouts[idx]) {
          onLayout = function(index, e) {
            const layout = e.nativeEvent.layout;
            this.setState(state => {
              state.restLayouts[index] = layout;
              return state;
            });
          }.bind(this, idx);
        }
        return (
          <Circle
            key={key}
            id={key}
            openVal={this.state.openVal}
            onLayout={onLayout}
            restLayout={this.state.restLayouts[idx]}
            onActivate={this.setState.bind(this, {
              activeKey: key,
              activeInitialLayout: this.state.restLayouts[idx],
            })}
          />
        );
      }
    });
    if (this.state.activeKey) {
      circles.push(
        <Animated.View
          key="dark"
          style={[styles.darkening, {opacity: this.state.openVal}]}
        />,
      );
      circles.push(
        <Circle
          openVal={this.state.openVal}
          key={this.state.activeKey}
          id={this.state.activeKey}
          restLayout={this.state.activeInitialLayout}
          containerLayout={this.state.layout}
          onMove={this._onMove}
          onDeactivate={() => {
            this.setState({activeKey: undefined});
          }}
        />,
      );
    }
    return (
      <View style={styles.container}>
        <View
          style={styles.grid}
          onLayout={e => this.setState({layout: e.nativeEvent.layout})}>
          {circles}
        </View>
      </View>
    );
  }

  _onMove(position: Point): void {
    const newKeys = moveToClosest(this.state, position);
    if (newKeys !== this.state.keys) {
      LayoutAnimation.easeInEaseOut(); // animates layout update as one batch (step3: uncomment)
      this.setState({keys: newKeys});
    }
  }
}

type Point = {
  x: number,
  y: number,
  ...
};
function distance(p1: Point, p2: Point): number {
  const dx = p1.x - p2.x;
  const dy = p1.y - p2.y;
  return dx * dx + dy * dy;
}

function moveToClosest({activeKey, keys, restLayouts}, position) {
  const activeIdx = -1;
  let closestIdx = activeIdx;
  let minDist = Infinity;
  const newKeys = [];
  keys.forEach((key, idx) => {
    const dist = distance(position, restLayouts[idx]);
    if (key === activeKey) {
      idx = activeIdx;
    } else {
      newKeys.push(key);
    }
    if (dist < minDist) {
      minDist = dist;
      closestIdx = idx;
    }
  });
  if (closestIdx === activeIdx) {
    return keys; // nothing changed
  } else {
    newKeys.splice(closestIdx, 0, activeKey);
    return newKeys;
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  grid: {
    flex: 1,
    justifyContent: 'center',
    flexDirection: 'row',
    flexWrap: 'wrap',
    backgroundColor: 'transparent',
  },
  circle: {
    width: CIRCLE_SIZE,
    height: CIRCLE_SIZE,
    borderRadius: CIRCLE_SIZE / 2,
    borderWidth: 1,
    borderColor: 'black',
    margin: CIRCLE_MARGIN,
    overflow: 'hidden',
  },
  dragView: {
    shadowRadius: 10,
    shadowColor: 'rgba(0,0,0,0.7)',
    shadowOffset: {height: 8},
    alignSelf: 'flex-start',
    backgroundColor: 'transparent',
  },
  open: {
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0,
    width: undefined, // unset value from styles.circle
    height: undefined, // unset value from styles.circle
    borderRadius: 0, // unset value from styles.circle
  },
  darkening: {
    backgroundColor: 'black',
    position: 'absolute',
    left: 0,
    top: 0,
    right: 0,
    bottom: 0,
  },
});

exports.title = 'Animated - Gratuitous App';
exports.description =
  'Bunch of Animations - tap a circle to open a view with more animations, or longPress and drag to reorder circles.';
exports.examples = [
  {
    title: 'And example app',
    render(): React.Element<typeof AnExApp> {
      return <AnExApp />;
    },
  },
];
