import React from 'react';
import {bool, number, object, string} from 'prop-types';


const initialState = {
  terminatingAnimationTimeout: null,
  percent: 0,
  progressInterval: null
};

const DEFAULT_SCOPE = 'default';
const UPDATE_TIME = 200;
const MAX_PROGRESS = 99;
const PROGRESS_INCREASE = 10;
export class LoadingBar extends React.Component {
  static UPDATE_TIME = UPDATE_TIME;
  static MAX_PROGRESS = MAX_PROGRESS;
  static PROGRESS_INCREASE = PROGRESS_INCREASE;
  static ANIMATION_TIME = UPDATE_TIME * 4;
  static TERMINATING_ANIMATION_TIME = UPDATE_TIME / 2;

  static instances = {};

  static create = (opt = {}) => {
    return (action, next) => {
      if (action.type && !action.suppressGlobalProgress) {
        if (action.type.match(/\/start$/)) {
          LoadingBar.showLoading(opt.name);
        } else if (action.type.match(/\/success$/) || action.type.match(/\/error$/)) {
          LoadingBar.hideLoading(opt.name);
        }
      }
      next(action);
    };
  }

  static showLoading(scope = DEFAULT_SCOPE) {
    const instance = LoadingBar.instances[scope];
    if (instance && instance.handleUpdate) {
      instance.handleUpdate({
        loading: (instance.state.loading || 0) + 1
      });
    } else {
      LoadingBar.instances[scope] = (LoadingBar.instances[scope] || 0) + 1;
    }
  }

  static hideLoading(scope = DEFAULT_SCOPE) {
    const instance = LoadingBar.instances[scope];
    if (instance && instance.handleUpdate) {
      instance.handleUpdate({
        loading: Math.max(0, (instance.state.loading || 1) - 1)
      });
    }
  }

  static resetLoading(scope = DEFAULT_SCOPE) {
    const instance = LoadingBar.instances[scope];
    if (instance && instance.handleUpdate) {
      instance.handleUpdate({loading: 0});
    }
  }

  static propTypes = {
    className: string,
    loading: number,
    maxProgress: number,
    progressIncrease: number,
    showFastActions: bool,
    updateTime: number,
    // eslint-disable-next-line react/no-unused-prop-types
    scope: string,
    // eslint-disable-next-line react/forbid-prop-types
    style: object
  }

  static defaultProps = {
    className: '',
    loading: 0,
    maxProgress: MAX_PROGRESS,
    progressIncrease: PROGRESS_INCREASE,
    showFastActions: false,
    style: {},
    updateTime: UPDATE_TIME,
    scope: DEFAULT_SCOPE
  }

  constructor(props) {
    super(props);

    this.state = {
      ...initialState,
      loading: LoadingBar.instances[props.scope] === undefined ? props.loading : LoadingBar.instances[props.scope].loading
    };
    this.style = {
      opacity: '0',
      transform: 'scaleX(0)',
      transformOrigin: 'left',
      width: '100%',
      willChange: 'transform, opacity'
    };

    // Use default styling if there's no CSS class applied
    if (!this.props.className) {
      this.style.height = '3px';
      this.style.backgroundColor = 'red';
      this.style.position = 'absolute';
    }

    Object.assign(this.style, this.props.style);

    this.boundSimulateProgress = this
      .simulateProgress
      .bind(this);
    this.boundResetProgress = this
      .resetProgress
      .bind(this);

    LoadingBar.instances[props.scope] = this;
  }

  componentDidMount() {
    // Re-render the component after mount to fix problems with SSR and CSP.
    //
    // Apps that use Server Side Rendering and has Content Security Policy for style
    // that doesn't allow inline styles should render an empty div and replace it
    // with the actual Loading Bar after mount See:
    // https://github.com/mironov/react-redux-loading-bar/issues/39
    //
    // eslint-disable-next-line react/no-did-mount-set-state
    this.mounted = true;

    if (this.state.loading > 0) {
      this.launch();
    }
  }

  componentWillUnmount() {
    this.mounted = false;
    clearInterval(this.state.progressInterval);
    clearTimeout(this.state.terminatingAnimationTimeout);
  }

  handleUpdate(nextProps) {
    if (this.shouldStart(nextProps)) {
      this.launch();
    } else if (this.shouldStop(nextProps)) {
      if (this.state.percent === 0 && !this.props.showFastActions) {
        // not even shown yet because the action finished quickly after start
        clearInterval(this.state.progressInterval);
        this.resetProgress();
      } else {
        // should progress to 100 percent
        this.setState({percent: 100});
      }
    }
  }

  // componentWillReceiveProps(nextProps) {   this.handleUpdate(nextProps); }

  shouldStart(nextProps) {
    if (!this.state.loading && nextProps.loading > 0) {
      this.setState({loading: nextProps.loading});
      return true;
    } else {
      return false;
    }
  }

  shouldStop(nextProps) {
    if (this.state.progressInterval && nextProps.loading === 0) {
      this.setState({loading: nextProps.loading});
      return true;
    } else {
      return false;
    }
  }

  shouldShow() {
    return this.state.percent > 0 && this.state.percent <= 100;
  }

  launch() {
    let {percent} = this.state;
    const {terminatingAnimationTimeout} = this.state;

    const loadingBarNotShown = !this._progressInterval;
    const terminatingAnimationGoing = percent === 100;

    if (loadingBarNotShown) {
      this._progressInterval = setInterval(this.boundSimulateProgress, this.props.updateTime);
    }

    if (terminatingAnimationGoing) {
      clearTimeout(terminatingAnimationTimeout);
    }

    percent = 0;

    this.setState({progressInterval: this._progressInterval, percent: percent});
  }

  newPercent() {
    const {percent} = this.state;
    const {progressIncrease} = this.props;

    // Use cos as a smoothing function Can be any function to slow down progress
    // near the 100%
    const smoothedProgressIncrease = (progressIncrease * Math.cos(percent * (Math.PI / 2 / 100)));

    return percent + smoothedProgressIncrease;
  }

  simulateProgress() {
    let {progressInterval, percent, terminatingAnimationTimeout} = this.state;
    const {maxProgress} = this.props;

    if (percent === 100) {
      clearInterval(progressInterval);
      terminatingAnimationTimeout = setTimeout(this.boundResetProgress, LoadingBar.TERMINATING_ANIMATION_TIME);
      progressInterval = null;
    } else if (this.newPercent() <= maxProgress) {
      percent = this.newPercent();
    }

    this.setState({percent, progressInterval, terminatingAnimationTimeout});
  }

  resetProgress() {
    this.setState(initialState);
  }

  buildStyle() {
    const animationTime = (this.state.percent !== 100 ? LoadingBar.ANIMATION_TIME : LoadingBar.TERMINATING_ANIMATION_TIME);

    this.style = Object.assign({}, this.style, {
      transform: `scaleX(${this.state.percent / 100})`,
      transition: `transform ${animationTime}ms linear`,
      opacity: this.shouldShow() ? '1' : '0'
    });
    return this.style;
  }

  render() {
    // In order not to violate strict style CSP it's better to make an extra
    // re-render after component mount
    if (!this.mounted) {
      return (<div />);
    }

    return (
      <div>
        <div style={this.buildStyle()} className={this.props.className} />
        <div style={{
          display: 'table',
          clear: 'both'
        }} />
      </div>
    );
  }
}
