'use strict';

import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import _ from 'lodash';
import {Icon} from '../Icon';
import {Block} from '../Block';
import {DangerousHTML} from '../DangerousHTML';

import {formatSeconds} from './utils';
import {RecorderLib, getRecorderInputVolume} from './RecorderLib';
import {VolumeControl} from './VolumeControl';
import {AudioPlayer} from './AudioPlayer';
import {Button} from '../Button';

const METER_FULL_VALUE = 28;
const METER_BOUNDARY = 5;
export class RecorderVolumeMeter extends React.Component {
    static propTypes = {
        id: PropTypes.string,
        scriptProcessorNode: PropTypes.object,
        volumeGainNode: PropTypes.object,
        analyser: PropTypes.object,
        meterCount: PropTypes.number,
        startRecording: PropTypes.func,
        startRecordingLabel: PropTypes.string
    };

    static defaultProps = {
        id: '',
        analyser: null,
        scriptProcessorNode: null,
        volumeGainNode: null,
        startRecording: () => {},
        meterCount: 12,
        startRecordingLabel: 'TRANSLATE:START_RECORDING'
    };

    constructor(props) {
        super(props);

        let {analyser, scriptProcessorNode, volumeGainNode, meterCount} = props;

        let volumeNodeGainValue = volumeGainNode.gain.value;
        this.ended = false;

        this.data = new Array(meterCount);

        this.state = {
            volume: 1,
            reset: false,
            volumeNodeGainValue: volumeNodeGainValue,
            isMuted: volumeNodeGainValue === 0
        };

        scriptProcessorNode.onaudioprocess = () => {
            if (this.ended) {
                return;
            }

            let {reset, volume, isMuted, volumeNodeGainValue} = this.state;
            let currentVolume;
            if (isMuted) {
                volumeNodeGainValue = 0;
            }
            if (volumeNodeGainValue === 0) {
                currentVolume = 0;
            } else {
                currentVolume = getRecorderInputVolume(analyser, volume, reset);
            }

            this.setState({
                volume: currentVolume,
                reset: false
            });
        };
    }

    componentWillUnmount() {
        this.ended = true;
    }

    getRecordButton() {
        return (
            <Button
                aria-label={this.props.startRecordingLabel}
                className="rc-audio-icon-wrapper rc-audio-record"
                testAutomationId={`audioRecord${this.props.id}`}
                onClick={() => {
                    this.props.startRecording();
                }}>
                <Icon name="record" />
            </Button>
        );
    }

    getMeter() {
        let {volume} = this.state;
        let {meterCount} = this.props;

        let volumeCount = Math.min(volume / (METER_FULL_VALUE / meterCount), meterCount);
        let className = volumeCount < METER_BOUNDARY ? 'volume-unqualified' : 'volume-qualified';
        for (let i = 0; i < meterCount; i++) {
            //init value
            this.data[i] = i <= volumeCount && volume > 0;
        }
        return (
            <div className="volume-meter">
                {_.map(this.data, (enabled, index) => {
                    let qualifiedClass = {};
                    qualifiedClass[className] = enabled;
                    return (
                        <div
                            className={classnames('volume-meter-unit', qualifiedClass)}
                            key={`volume-meter-unit-${index + 1}`}
                        />
                    );
                })}
            </div>
        );
    }

    getVolumeControl() {
        let {volumeNodeGainValue, isMuted} = this.state;
        return (
            <VolumeControl
                volume={volumeNodeGainValue}
                isMuted={isMuted}
                setVolume={this.setInputMaxVolume}
            />
        );
    }

    setInputMaxVolume = (value, isMuted) => {
        let {volumeGainNode} = this.props;
        let {volumeNodeGainValue} = this.state;

        if (_.isUndefined(isMuted)) {
            if (!value) {
                value = volumeNodeGainValue;
                isMuted = true;
            } else {
                isMuted = false;
            }
        } else if (!isMuted) {
            value = value || 1;
        }

        volumeGainNode.gain.value = isMuted ? 0 : value;
        this.setState({
            volumeNodeGainValue: value,
            isMuted,
            reset: true
        });
    };

    render() {
        return (
            <div className="rc-audio-recorder">
                {this.getRecordButton()}
                {this.getMeter()}
                {this.getVolumeControl()}
            </div>
        );
    }
}

const PROGRESS_WAVE = 'progress-wave';
export class RecorderProcessor extends React.Component {
    static propTypes = {
        id: PropTypes.string,
        analyser: PropTypes.object,
        recorder: PropTypes.object,
        scriptProcessorNode: PropTypes.object,
        meterMinHeight: PropTypes.number,
        meterMaxHeight: PropTypes.number,
        maxRecordTime: PropTypes.number,
        stopRecording: PropTypes.func,
        stopRecordingLabel: PropTypes.string
    };

    static defaultProps = {
        id: '',
        analyser: null,
        recorder: null,
        scriptProcessorNode: null,
        stopRecording: () => {},
        meterMaxHeight: 15,
        meterMinHeight: 5,
        maxRecordTime: 300, //5min
        stopRecordingLabel: 'TRANSLATE:STOP_RECORDING'
    };

    constructor(props) {
        super(props);

        this.state = {
            inputVolume: 0,
            currentTime: 0
        };

        this.initTime = 0;
        this.data = [];
    }

    componentWillMount() {
        let {recorder, scriptProcessorNode, analyser, stopRecording, maxRecordTime} = this.props;
        scriptProcessorNode.onaudioprocess = e => {
            recorder.onaudioprocess(e);
            if (!recorder.recording) {
                return;
            }
            let {inputVolume} = this.state;
            let currentTime = e.playbackTime;
            if (!this.initTime) {
                this.initTime = currentTime;
            }
            currentTime -= this.initTime;
            if (currentTime < maxRecordTime) {
                inputVolume = getRecorderInputVolume(analyser, inputVolume, false);
                this.data.push(inputVolume);
                if (this.maxMeterCount && this.data.length > this.maxMeterCount) {
                    this.data.shift();
                }
                if (!this.unmounted) {
                    this.setState({
                        inputVolume,
                        currentTime
                    });
                }
            } else {
                if (this.unmounted) {
                    stopRecording();
                }
            }
        };
        recorder.record();
    }

    componentDidMount() {
        let progressWaveLength = this.refs[PROGRESS_WAVE].offsetWidth;
        this.maxMeterCount = (progressWaveLength + 1) / 2;
    }

    componentWillUnmount() {
        this.unmounted = true;
    }

    getStopRecordButton() {
        return (
            <Button
                aria-label={this.props.stopRecordingLabel}
                className="rc-audio-icon-wrapper rc-record-complete"
                testAutomationId={`audioStopRecord${this.props.id}`}
                onClick={() => {
                    this.props.stopRecording();
                }}>
                <Icon name="record-complete" />
            </Button>
        );
    }

    getWave() {
        let {meterMaxHeight, meterMinHeight} = this.props;

        let progressedMeter = _.map(this.data, (volume, index) => {
            let height =
                meterMinHeight +
                Math.min(volume, METER_FULL_VALUE) /
                    METER_FULL_VALUE *
                    (meterMaxHeight - meterMinHeight);
            return (
                <div
                    className="progress-volume-meter"
                    style={{left: index * 2 + 'px', height: height + 'px'}}
                    key={`progress-volume-meter-unit-${index}`}
                />
            );
        });
        return (
            <div className="progress-wav" ref={PROGRESS_WAVE}>
                {progressedMeter}
            </div>
        );
    }

    render() {
        let {currentTime} = this.state;
        return (
            <div className="rc-audio-recorder recorder-progressor">
                {this.getStopRecordButton()}
                {this.getWave()}
                <div className="audio-duration">
                    {formatSeconds(currentTime)} / {formatSeconds(this.props.maxRecordTime)}
                </div>
            </div>
        );
    }
}

const STEPS = {
    METER: 0,
    RECORDING: 1,
    RECORDED: 2
};
export class AudioRecorder extends React.Component {
    static propTypes = {
        unsupportText: PropTypes.string,
        volumeMeterTitle: PropTypes.string,
        volumeMeterText: PropTypes.string,
        processorTitle: PropTypes.string,
        processorText: PropTypes.string,
        completedTitle: PropTypes.string,
        completedText: PropTypes.string,
        fileName: PropTypes.string,
        audioRecordError: PropTypes.string,
        deviceNotFoundError: PropTypes.string,
        config: PropTypes.object,
        exportAudio: PropTypes.func,
        onRecordingStopped: PropTypes.func,
        uploadLabel: PropTypes.string
    };

    static defaultProps = {
        exportAudio: null,
        onRecordingStopped: null,
        config: {
            bufferLen: 2048, //4096,
            numChannels: 1, //from 2 to 1, to reduce audio size
            mimeType: 'audio/wav'
        },
        fileName: 'audio.wav',
        id: 'audio-recorder',
        unsupportText: 'TRANSLATE:MICROPHONE_NOT_SUPPORTED',
        volumeMeterTitle: 'TRANSLATE:MICROPHONE_TEST',
        volumeMeterText: 'TRANSLATE:MICROPHONE_TEST_TEXT',
        processorTitle: 'TRANSLATE:RECORDING',
        processorText: 'TRANSLATE:RECORDING_IN_PROGRESS',
        completedTitle: 'TRANSLATE:RECORDING',
        completedText: 'TRANSLATE:MICROPHONE_RECORD_COMPLETE',
        deviceNotFoundError: 'TRANSLATE:MICROPHONE_NOT_FOUND_ERROR',
        audioRecordError: 'TRANSLATE:MICROPHONE_RECORD_ERROR',
        uploadLabel: 'TRANSLATE:UPLOAD'
    };

    constructor(props) {
        super(props);

        this.state = {
            step: STEPS.METER,
            ready: false
        };
    }

    componentWillMount() {
        if (audioRecorderSupported()) {
            let {audioRecordError, deviceNotFoundError} = this.props;
            window.audioContext = window.audioContext || new AudioContext();
            this.audioContext = window.audioContext;

            navigator.mediaDevices
                .getUserMedia({audio: true})
                .then(stream => {
                    this.localStream = stream;
                    let {config} = this.props;
                    this.mediaStream = this.audioContext.createMediaStreamSource(stream);

                    this.volumeGainNode = this.audioContext.createGain();
                    this.mediaStream.connect(this.volumeGainNode);

                    this.analyser = this.audioContext.createAnalyser();
                    this.analyser.fftSize = config.bufferLen / config.numChannels;
                    this.volumeGainNode.connect(this.analyser);

                    this.scriptProcessorNode = this.audioContext.createScriptProcessor(
                        config.bufferLen,
                        config.numChannels,
                        config.numChannels
                    );
                    this.analyser.connect(this.scriptProcessorNode);
                    this.scriptProcessorNode.connect(this.audioContext.destination); //this should not be necessary

                    this.setState({
                        ready: true
                    });
                })
                .catch(e => {
                    if (
                        e.name === 'DevicesNotFoundError' || //for Chrome
                        e.name === 'NotFoundError'
                    ) {
                        //for firefox
                        this.errorText = deviceNotFoundError;
                    } else {
                        this.errorText = audioRecordError || e.name;
                    }
                    // eslint-disable-next-line
                    console.log(e);
                    this.setState({
                        ready: true
                    });
                });
        }
    }

    componentWillUnmount() {
        if (audioRecorderSupported() && this.mediaStream) {
            this.mediaStream.disconnect(this.volumeGainNode);
            this.volumeGainNode.disconnect(this.analyser);
            this.analyser.disconnect(this.scriptProcessorNode);
            this.scriptProcessorNode.disconnect(this.audioContext.destination);
            delete this.scriptProcessorNode;
            delete this.audioContext;

            _.forEach(this.localStream.getTracks(), track => {
                track.stop();
            });
        }
    }

    getText(title, text) {
        let titleCmp = title ? (
            <Block condensed>
                <strong>{title}</strong>
            </Block>
        ) : null;
        return (
            <div>
                {titleCmp}
                <Block>
                    <DangerousHTML>{text}</DangerousHTML>
                </Block>
            </div>
        );
    }

    getUploadButton() {
        return this.state.step === STEPS.RECORDED ? (
            <div className="rc-audio-upload">
                <a
                    role="button"
                    aria-label={this.props.uploadLabel}
                    href="#"
                    onClick={e => {
                        e.preventDefault();
                        this.uploadAudio();
                    }}>
                    <Icon name="circle-upload" />
                </a>
            </div>
        ) : null;
    }

    createRecorder() {
        return new RecorderLib(this.props.config, this.audioContext.sampleRate);
    }

    startRecording() {
        this.recorder = this.recorder || this.createRecorder();

        this.setState({
            step: STEPS.RECORDING
        });
    }

    stopRecording() {
        if (this.recorder) {
            this.recorder.stop();
            this.recorder.exportWAV(audioBlobData => {
                let {onRecordingStopped} = this.props;

                if (onRecordingStopped) {
                    onRecordingStopped(audioBlobData);
                }

                this.audioBlobData = audioBlobData;
                this.recorder.clear();
                this.setState({
                    step: STEPS.RECORDED
                });
            });
        }
    }

    uploadAudio() {
        if (this.audioBlobData) {
            let {exportAudio, fileName} = this.props;
            let formData = new FormData();
            formData.append('file', this.audioBlobData, fileName);
            if (exportAudio) {
                exportAudio(formData);
            }
        }
    }

    render() {
        let {
            unsupportText,
            volumeMeterTitle,
            volumeMeterText,
            processorTitle,
            processorText,
            completedTitle,
            completedText
        } = this.props;
        let {ready, step} = this.state;

        if (!audioRecorderSupported()) {
            return this.getText('', unsupportText);
        }

        if (!ready) {
            return null;
        }

        if (this.errorText) {
            return this.getText('', this.errorText);
        }

        let recorderStep = null;
        let promptText = null;

        switch (step) {
            case STEPS.METER:
                promptText = this.getText(volumeMeterTitle, volumeMeterText);
                recorderStep = (
                    <RecorderVolumeMeter
                        scriptProcessorNode={this.scriptProcessorNode}
                        volumeGainNode={this.volumeGainNode}
                        analyser={this.analyser}
                        startRecording={() => {
                            this.startRecording();
                        }}
                    />
                );
                break;
            case STEPS.RECORDING:
                promptText = this.getText(processorTitle, processorText);
                recorderStep = (
                    <RecorderProcessor
                        scriptProcessorNode={this.scriptProcessorNode}
                        analyser={this.analyser}
                        recorder={this.recorder}
                        stopRecording={() => {
                            this.stopRecording();
                        }}
                    />
                );
                break;
            case STEPS.RECORDED:
                promptText = this.getText(completedTitle, completedText);
                let audioUrl = this.audioBlobData ? URL.createObjectURL(this.audioBlobData) : '';
                recorderStep = audioUrl ? (
                    <AudioPlayer
                        audioUrl={audioUrl}
                        onRecord={() => {
                            this.setState({
                                step: STEPS.METER
                            });
                        }}
                    />
                ) : null;
                break;
            default:
                break;
        }
        return (
            <div>
                {promptText}
                <div className="rc-audio">
                    {recorderStep}
                    {this.getUploadButton()}
                </div>
            </div>
        );
    }
}

export function audioRecorderSupported() {
    window.AudioContext = window.AudioContext || window.webkitAudioContext;
    window.URL = window.URL || window.webkitURL;
    return (
        window.AudioContext &&
        window.URL &&
        navigator.mediaDevices &&
        navigator.mediaDevices.getUserMedia
    );
}
