/**
 * 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 WarningBox
 */
'use strict';

var AsyncStorage = require('AsyncStorage');
var EventEmitter = require('EventEmitter');
var Map = require('Map');
var PanResponder = require('PanResponder');
var React = require('React');
var StyleSheet = require('StyleSheet');
var Text = require('Text');
var TouchableOpacity = require('TouchableOpacity');
var View = require('View');

var invariant = require('invariant');
var rebound = require('rebound');
var stringifySafe = require('stringifySafe');

var SCREEN_WIDTH = require('Dimensions').get('window').width;
var IGNORED_WARNINGS_KEY = '__DEV_WARNINGS_IGNORED';

var consoleWarn = console.warn.bind(console);

var warningCounts = new Map();
var ignoredWarnings: Array<string> = [];
var totalWarningCount = 0;
var warningCountEvents = new EventEmitter();

/**
 * WarningBox renders warnings on top of the app being developed. Warnings help
 * guard against subtle yet significant issues that can impact the quality of
 * your application, such as accessibility and memory leaks. This "in your
 * face" style of warning allows developers to notice and correct these issues
 * as quickly as possible.
 *
 * The warning box is currently opt-in. Set the following flag to enable it:
 *
 * `console.yellowBoxEnabled = true;`
 *
 * If "ignore" is tapped on a warning, the WarningBox will record that warning
 * and will not display it again. This is useful for hiding errors that already
 * exist or have been introduced elsewhere. To re-enable all of the errors, set
 * the following:
 *
 * `console.yellowBoxResetIgnored = true;`
 *
 * This can also be set permanently, and ignore will only silence the warnings
 * until the next refresh.
 */

if (__DEV__) {
  console.warn = function() {
    consoleWarn.apply(null, arguments);
    if (!console.yellowBoxEnabled) {
      return;
    }
    var warning = Array.prototype.map.call(arguments, stringifySafe).join(' ');
    if (!console.yellowBoxResetIgnored &&
        ignoredWarnings.indexOf(warning) !== -1) {
      return;
    }
    var count = warningCounts.has(warning) ? warningCounts.get(warning) + 1 : 1;
    warningCounts.set(warning, count);
    totalWarningCount += 1;
    warningCountEvents.emit('count', totalWarningCount);
  };
}

function saveIgnoredWarnings() {
  AsyncStorage.setItem(
    IGNORED_WARNINGS_KEY,
    JSON.stringify(ignoredWarnings),
    function(err) {
      if (err) {
        console.warn('Could not save ignored warnings.', err);
      }
    }
  );
}

AsyncStorage.getItem(IGNORED_WARNINGS_KEY, function(err, data) {
  if (!err && data && !console.yellowBoxResetIgnored) {
    ignoredWarnings = JSON.parse(data);
  }
});

var WarningRow = React.createClass({
  componentWillMount: function() {
    this.springSystem = new rebound.SpringSystem();
    this.dismissalSpring = this.springSystem.createSpring();
    this.dismissalSpring.setRestSpeedThreshold(0.05);
    this.dismissalSpring.setCurrentValue(0);
    this.dismissalSpring.addListener({
      onSpringUpdate: () => {
        var val = this.dismissalSpring.getCurrentValue();
        this.text && this.text.setNativeProps({
          left: SCREEN_WIDTH * val,
        });
        this.container && this.container.setNativeProps({
          opacity: 1 - val,
        });
        this.closeButton && this.closeButton.setNativeProps({
          opacity: 1 - (val * 5),
        });
      },
      onSpringAtRest: () => {
        if (this.dismissalSpring.getCurrentValue()) {
          this.collapseSpring.setEndValue(1);
        }
      },
    });
    this.collapseSpring = this.springSystem.createSpring();
    this.collapseSpring.setRestSpeedThreshold(0.05);
    this.collapseSpring.setCurrentValue(0);
    this.collapseSpring.getSpringConfig().friction = 20;
    this.collapseSpring.getSpringConfig().tension = 200;
    this.collapseSpring.addListener({
      onSpringUpdate: () => {
        var val = this.collapseSpring.getCurrentValue();
        this.container && this.container.setNativeProps({
          height: Math.abs(46 - (val * 46)),
        });
      },
      onSpringAtRest: () => {
        this.props.onDismissed();
      },
    });
    this.panGesture = PanResponder.create({
      onStartShouldSetPanResponder: () => {
        return !! this.dismissalSpring.getCurrentValue();
      },
      onMoveShouldSetPanResponder: () => true,
      onPanResponderGrant: () => {
        this.isResponderOnlyToBlockTouches =
          !! this.dismissalSpring.getCurrentValue();
      },
      onPanResponderMove: (e, gestureState) => {
        if (this.isResponderOnlyToBlockTouches) {
          return;
        }
        this.dismissalSpring.setCurrentValue(gestureState.dx / SCREEN_WIDTH);
      },
      onPanResponderRelease: (e, gestureState) => {
        if (this.isResponderOnlyToBlockTouches) {
          return;
        }
        var gestureCompletion = gestureState.dx / SCREEN_WIDTH;
        var doesGestureRelease = (gestureState.vx + gestureCompletion) > 0.5;
        this.dismissalSpring.setEndValue(doesGestureRelease ? 1 : 0);
      }
    });
  },
  render: function() {
    var countText;
    if (warningCounts.get(this.props.warning) > 1) {
      countText = (
        <Text style={styles.bold}>
          ({warningCounts.get(this.props.warning)}){" "}
        </Text>
      );
    }
    return (
      <View
        style={styles.warningBox}
        ref={container => { this.container = container; }}
        {...this.panGesture.panHandlers}>
        <TouchableOpacity
          onPress={this.props.onOpened}>
          <View>
            <Text
              style={styles.warningText}
              numberOfLines={2}
              ref={text => { this.text = text; }}>
              {countText}
              {this.props.warning}
            </Text>
          </View>
        </TouchableOpacity>
        <View
          ref={closeButton => { this.closeButton = closeButton; }}
          style={styles.closeButton}>
          <TouchableOpacity
            onPress={() => {
              this.dismissalSpring.setEndValue(1);
            }}>
            <Text style={styles.closeButtonText}>✕</Text>
          </TouchableOpacity>
        </View>
      </View>
    );
  }
});

var WarningBoxOpened = React.createClass({
  render: function() {
    var countText;
    if (warningCounts.get(this.props.warning) > 1) {
      countText = (
        <Text style={styles.bold}>
          ({warningCounts.get(this.props.warning)}){" "}
        </Text>
      );
    }
    return (
      <TouchableOpacity
        activeOpacity={0.9}
        onPress={this.props.onClose}>
        <View style={styles.yellowBox}>
          <Text style={styles.yellowBoxText}>
            {countText}
            {this.props.warning}
          </Text>
          <View style={styles.yellowBoxButtons}>
            <View style={styles.yellowBoxButton}>
              <TouchableOpacity
                onPress={this.props.onDismissed}>
                <Text style={styles.yellowBoxButtonText}>
                  Dismiss
                </Text>
              </TouchableOpacity>
            </View>
            <View style={styles.yellowBoxButton}>
              <TouchableOpacity
                onPress={this.props.onIgnored}>
                <Text style={styles.yellowBoxButtonText}>
                  Ignore
                </Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </TouchableOpacity>
    );
  },
});

var canMountWarningBox = true;

var WarningBox = React.createClass({
  getInitialState: function() {
    return {
      totalWarningCount,
      openWarning: null,
    };
  },
  componentWillMount: function() {
    if (console.yellowBoxResetIgnored) {
      AsyncStorage.setItem(IGNORED_WARNINGS_KEY, '[]', function(err) {
        if (err) {
          console.warn('Could not reset ignored warnings.', err);
        }
      });
      ignoredWarnings = [];
    }
  },
  componentDidMount: function() {
    invariant(
      canMountWarningBox,
      'There can only be one WarningBox'
    );
    canMountWarningBox = false;
    warningCountEvents.addListener(
      'count',
      this._onWarningCount
    );
  },
  componentWillUnmount: function() {
    warningCountEvents.removeAllListeners();
    canMountWarningBox = true;
  },
  _onWarningCount: function(totalWarningCount) {
    // Must use setImmediate because warnings often happen during render and
    // state cannot be set while rendering
    setImmediate(() => {
      this.setState({ totalWarningCount, });
    });
  },
  _onDismiss: function(warning) {
    warningCounts.delete(warning);
    this.setState({
      openWarning: null,
    });
  },
  render: function() {
    if (warningCounts.size === 0) {
      return <View />;
    }
    if (this.state.openWarning) {
      return (
        <WarningBoxOpened
          warning={this.state.openWarning}
          onClose={() => {
            this.setState({ openWarning: null });
          }}
          onDismissed={this._onDismiss.bind(this, this.state.openWarning)}
          onIgnored={() => {
            ignoredWarnings.push(this.state.openWarning);
            saveIgnoredWarnings();
            this._onDismiss(this.state.openWarning);
          }}
        />
      );
    }
    var warningRows = [];
    warningCounts.forEach((count, warning) => {
      warningRows.push(
        <WarningRow
          key={warning}
          onOpened={() => {
            this.setState({ openWarning: warning });
          }}
          onDismissed={this._onDismiss.bind(this, warning)}
          warning={warning}
        />
      );
    });
    return (
      <View style={styles.warningContainer}>
        {warningRows}
      </View>
    );
  },
});

var styles = StyleSheet.create({
  bold: {
    fontWeight: 'bold',
  },
  closeButton: {
    position: 'absolute',
    right: 0,
    height: 46,
    width: 46,
  },
  closeButtonText: {
    color: 'white',
    fontSize: 32,
    position: 'relative',
    left: 8,
  },
  warningContainer: {
    position: 'absolute',
    left: 0,
    right: 0,
    bottom: 0
  },
  warningBox: {
    position: 'relative',
    backgroundColor: 'rgba(171, 124, 36, 0.9)',
    flex: 1,
    height: 46,
  },
  warningText: {
    color: 'white',
    position: 'absolute',
    left: 0,
    marginLeft: 15,
    marginRight: 46,
    top: 7,
  },
  yellowBox: {
    backgroundColor: 'rgba(171, 124, 36, 0.9)',
    position: 'absolute',
    left: 0,
    right: 0,
    top: 0,
    bottom: 0,
    padding: 15,
    paddingTop: 35,
  },
  yellowBoxText: {
    color: 'white',
    fontSize: 20,
  },
  yellowBoxButtons: {
    flexDirection: 'row',
    position: 'absolute',
    bottom: 0,
  },
  yellowBoxButton: {
    flex: 1,
    padding: 25,
  },
  yellowBoxButtonText: {
    color: 'white',
    fontSize: 16,
  }
});

module.exports = WarningBox;
