/*
 * Copyright 2018, 2019, 2020 Dooboolab.
 *
 * This file is part of Flutter-Sound.
 *
 * Flutter-Sound is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3 (LGPL-V3), as published by
 * the Free Software Foundation.
 *
 * Flutter-Sound is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Flutter-Sound.  If not, see <https://www.gnu.org/licenses/>.
 */

const RECORDER_VERSION = '9.5.0'

const IS_RECORDER_PAUSED = 1;
const IS_RECORDER_RECORDING = 2;
const IS_RECORDER_STOPPED = 0;

function newRecorderInstance(aCallback, callbackTable) { return new FlutterSoundRecorder(aCallback, callbackTable); }


const CB_updateRecorderProgress = 0;
const CB_recordingData = 1;
const CB_startRecorderCompleted = 2;
const CB_pauseRecorderCompleted = 3;
const CB_resumeRecorderCompleted = 4;
const CB_stopRecorderCompleted = 5;
const CB_openRecorderCompleted = 6;
const CB_closeRecorderCompleted = 7;
const CB_recorder_log = 8;

class FlutterSoundRecorder {
        static newInstance(aCallback, callbackTable) { return new FlutterSoundRecorder(aCallback, callbackTable); }

        constructor(aCallback, callbackTable) {
                this.callback = aCallback;
                this.callbackTable = callbackTable;
                this.subscriptionDuration = 0;
                this.timerId = null;
                this.deltaTime = 0;
                this.currentRecordPath = '';
                this.localObjects = [];
                this.instanceNo = instanceNumber;
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'Instance Number : ' + this.instanceNo.toString())
                ++instanceNumber;
        }



        initializeFlautoRecorder(focus, category, mode, audioFlags, device) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'initializeFlautoRecorder');
                this.callbackTable[CB_openRecorderCompleted](this.callback, IS_RECORDER_STOPPED, true);

        }


        deleteObjects() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:---> deleteObjects ');
                for (var url in this.localObjects) {
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'deleteRecord : ' + url);
                        this.deleteRecord(url);
                }
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:<--- deleteObjects ');
        }


        releaseFlautoRecorder() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:---> releaseFlautoRecorder');
                this.stop();
                this.deleteObjects();
                this.localObjects = [];

                this.callbackTable[CB_closeRecorderCompleted](this.callback, IS_RECORDER_STOPPED, true);
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:<--- releaseFlautoRecorder');
        }


        setAudioFocus(focus, category, mode, audioFlags, device) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'setAudioFocus');
        }


        isEncoderSupported(codec) {
                /*
                                for (var i in mime_types)
                                {
                                }
                */
                var r = MediaRecorder.isTypeSupported(mime_types[codec]);
                if (r)
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'mime_types[codec] encoder is supported');
                else
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'mime_types[codec] encoder is NOT supported');
                return r;
        }


        setSubscriptionDuration(duration) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'setSubscriptionDuration');
                this.subscriptionDuration = duration;
                if (this.mediaRecorder != null)
                        this.startTimer();
        }


        _deleteRecord(aPath,) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'deleteRecord: ' + aPath);
                if ((aPath == null) || (aPath == '')) {
                        path = lasturl;
                } else {
                        var path = aPath;
                }
                var myStorage;
                if (path.substring(0, 1) == '/') {
                        myStorage = window.localStorage;
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'localStorage');
                } else {
                        myStorage = window.sessionStorage;
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'sessionStorage');
                }
                var oldUrl = myStorage.getItem(path);
                if (oldUrl != null && oldUrl != '') {
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'Deleting object  : ' + oldUrl.toString());
                        URL.revokeObjectURL(oldUrl);
                        return true;
                }

                return false;
        }



        deleteRecord(path) {
                this._deleteRecord(path);

                var found = this.localObjects.findIndex(element => element == path);
                if (found != null && found >= 0) {
                        this.callbackTable[CB_recorder_log](this.callback, DBG, "Found : " + found);
                        this.localObjects[found] = null;
                }

        }

        setRecordURL(aPath, newUrl) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'setRecordUrl: ' + aPath + ' <- ' + newUrl);
                var path = aPath;
                var myStorage;
                if ((path == null) || (path == '')) {
                        return null;
                }
                if (path.substring(0, 1) == '/') {
                        myStorage = window.localStorage;
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'localStorage');
                } else {
                        myStorage = window.sessionStorage;
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'sessionStorage');
                }
                var oldUrl = myStorage.getItem(path);
                if (oldUrl != null && oldUrl != '') {
                        this.callbackTable[CB_recorder_log](this.callback, DBG, 'Deleting object ' + ' : ' + oldUrl.toString());
                        URL.revokeObjectURL(oldUrl);
                } else {

                }

                lastUrl = aPath;

                myStorage.setItem(path, newUrl);
                this.callbackTable[CB_recorder_log](this.callback, DBG, '<--- setRecordURL ( ' + path + ' ) : ' + newUrl);

        }

        getRecordURL(aPath,) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, '---> getRecordURL : ' + aPath);
                var r = getRecordURL(aPath);
                if (r == null)
                        r = ''; // stopRecorder does not like a null
                this.callbackTable[CB_recorder_log](this.callback, DBG, '<--- getRecordURL :' + r);
                return r;
        }


        async startRecorder(path, sampleRate, numChannels, bitRate, bufferSize, enableVoiceProcessing ,codec, toStream, audioSource) {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'startRecorder');
                //var constraints = { audio: true};
                //var chunks ;//= [];
                var me = this;
                this.currentRecordPath = path;
                var chunks = [];
                var mediaStream;
                mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
                me.mediaStream = mediaStream;

                const audioContext = new AudioContext();
                const _audioSource = audioContext.createMediaStreamSource(mediaStream);
                const analyser = audioContext.createAnalyser();
                // todo: review if this values are right (set to mimic a behaviour closest to that of Android)
                analyser.fftSize = 512;
                analyser.minDecibels = -110;
                analyser.maxDecibels = 0;
                analyser.smoothingTimeConstant = 0.4;
                _audioSource.connect(analyser);
                const volumes = new Uint8Array(analyser.frequencyBinCount);

                this.getVolumeLevel = () => {
                        analyser.getByteFrequencyData(volumes);
                        let volumeSum = 0;
                        for (const volume of volumes)
                                volumeSum += volume;
                        return volumeSum / volumes.length;
                }


                //navigator.mediaDevices.getUserMedia(constraints).then
                //(function(mediaStream)
                //{
                /*
                        var audioCtx = new AudioContext();


                        var source = audioCtx.createMediaStreamSource(mediaStream);
                        //var dest = new audioCtx.createMediaStreamDestination();
                        var offlineCtx = new OfflineAudioContext(2,44100*40,44100);
                        var sourceOfflineCtx = offlineCtx.createBufferSource();
                        //source.connect(sourceOfflineCtx);
                        //source.connect(offlineCtx.source);
                        //source.start();
                        offlineCtx.startRendering().then(function(renderedBuffer)
                        {
                                //this.caller.toto(chunks);
                                onStop(renderedBuffer);
                        }
                        ).catch(function(err)
                        {
                                // Note: The promise should reject when startRendering is called a second time on an OfflineAudioContext
                        }
                        );
*/

                //       var buffer = audioCtx.createBuffer(numChannels, tenMinutes, sampleRate); // Play back ???
                //      source.connect(audioCtx.destination); // This ouput to speaker // TODO : not yet supported


                // ===========================================================================

                var options =
                {
                        audioBitsPerSecond: bitRate,
                        mimeType: mime_types[codec]
                }

                var mediaRecorder = new MediaRecorder(mediaStream, options);
                me.mediaRecorder = mediaRecorder;
                if (toStream) // not yet implemented !
                        mediaRecorder.start(30); // 30 milliseconds for a chunk
                else
                        mediaRecorder.start();
                me.callbackTable[CB_recorder_log](me.callback, DBG, "recorder started : " + mediaRecorder.state);


                mediaRecorder.ondataavailable = function (e) {
                        if (e.data) {
                                if (toStream) // not yet implemented !
                                {
                                        me.callbackTable[CB_recordingData](me.callback, e.data);

                                }
                                if (path != null && path != '') {
                                        me.callbackTable[CB_recorder_log](me.callback, DBG, 'On data available : ' + e.data.constructor.name);
                                        chunks.push(e.data);
                                }
                        }
                }

                mediaRecorder.onstart = function (e) {
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '---> mediaRecorder OnStart');
                        me.deltaTime = 0;
                        me.startTimer();
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '<---mediaRecorder OnStart : ' + me.mediaRecorder.state);
                        me.callbackTable[CB_startRecorderCompleted](me.callback, IS_RECORDER_RECORDING, true);

                }

                mediaRecorder.onerror = function (e) {
                        me.callbackTable[CB_recorder_log](me.callback, DBG, "mediaRecorder OnError : " + e.error);
                        me.stopRecorder()
                }

                mediaRecorder.onpause = function (e) {
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '---> mediaRecorder onpause');
                        me.callbackTable[CB_pauseRecorderCompleted](me.callback, IS_RECORDER_PAUSED, true);
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '<--- mediaRecorder onpause');
                }

                mediaRecorder.onresume = function (e) {
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '---> mediaRecorder onresume');
                        me.callbackTable[CB_resumeRecorderCompleted](me.callback, IS_RECORDER_RECORDING, true);
                        me.callbackTable[CB_recorder_log](me.callback, DBG, '<--- mediaRecorder onresume');
                }

                mediaRecorder.onstop = function (e) {

                        me.callbackTable[CB_recorder_log](me.callback, DBG, '---> mediaRecorder onstop');
                        var blob = new Blob(chunks, { 'type': mime_types[codec] });
                        var url = URL.createObjectURL(blob);
                        me.callbackTable[CB_recorder_log](me.callback, DBG, 'Instance Number : ' + me.instanceNo.toString())

                        me.setRecordURL(path, url);

                        var found = me.localObjects.findIndex(element => element == path);
                        if (found != null && found >= 0) {
                                me.callbackTable[CB_recorder_log](me.callback, DBG, "Found : " + found);
                                me.localObjects[found] = path;
                        } else {
                                me.callbackTable[CB_recorder_log](me.callback, DBG, "NOT FOUND! : " + path);
                                me.localObjects.push(path);
                        }


                        /*
                                                                var xhr = new XMLHttpRequest();
                                                                var blob;
                                                                var fileReader = new FileReader();
                                                                xhr.open("GET", url, true);
                                                                xhr.responseType = "arraybuffer";
                        
                        
                                                                xhr.addEventListener("load", function ()
                                                                {
                                                                        if (xhr.status === 200)
                                                                        {
                                                                                // Create a blob from the response
                                                                                blob = new Blob([xhr.response], {type: "audio/webm\;codecs=opus"});
                        
                                                                                // onload needed since Google Chrome doesn't support addEventListener for FileReader
                                                                                fileReader.onload = function (evt)
                                                                                {
                                                                                        // Read out file contents as a Data URL
                                                                                        var result = evt.target.result;
                                                                                        // Set image src to Data URL
                                                                                        //rhino.setAttribute("src", result);
                                                                                        // Store Data URL in localStorage
                                                                                        try
                                                                                        {
                                                                                                //localStorage.setItem("rhino", result);
                                                                                                onStop(result);
                                                                                                myStorage.setItem(path, JSON.stringify(result));
                                                                                        } catch (e)
                                                                                        {
                                                                                        }
                                                                                };
                                                                                // Load blob as Data URL
                                                                                fileReader.readAsDataURL(blob);
                                                                        }
                                                                }, false);
                                                                // Send XHR
                                                                xhr.send();
                                */
                        chunks = null;///[];
                        me.callbackTable[CB_recorder_log](me.callback, DBG, 'recorder stopped');
                        me.mediaRecorder = null;
                        me.callbackTable[CB_stopRecorderCompleted](me.callback, IS_RECORDER_STOPPED, true, me.getRecordURL(path));

                        me.callbackTable[CB_recorder_log](me.callback, DBG, '<--- mediaRecorder onstop');
                }
                //});
        }

        stop() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:---> stop()');
                this.stopTimer();
                if (this.mediaRecorder != null) {
                        this.mediaRecorder.stop();
                        this.mediaRecorder = null;
                }
                if (this.mediaStream != null) {
                        this.mediaStream.getTracks().forEach(track => track.stop());
                        this.mediaStream = null;
                }
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:<--- stop()');
        }

        stopRecorder() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:---> stopRecorder');
                this.stopTimer();
                if (this.mediaRecorder != null) {
                        this.mediaRecorder.stop();
                } else {
                        this.callbackTable[CB_stopRecorderCompleted](this.callback, IS_RECORDER_STOPPED, /*false*/true, this.getRecordURL(this.currentRecordPath));
                }
                this.mediaRecorder = null;

                if (this.mediaStream != null) {
                        this.mediaStream.getTracks().forEach(track => track.stop());
                        this.mediaStream = null;
                }
                this.callbackTable[CB_recorder_log](this.callback, DBG, "JS:<--- stopRecorder");
        }


        pauseRecorder() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'pauseRecorder');
                this.mediaRecorder.pause();
                this.stopTimer();
                this.callbackTable[CB_recorder_log](this.callback, DBG, "recorder paused : " + this.mediaRecorder.state);

        }


        resumeRecorder() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'resumeRecorder');
                this.mediaRecorder.resume();
                this.startTimer();
                this.callbackTable[CB_recorder_log](this.callback, DBG, "recorder resumed : " + this.mediaRecorder.state);
        }

        startTimer() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'startTimer()');
                this.stopTimer();
                var me = this;

                if (this.subscriptionDuration > 0) {
                        this.countDownDate = new Date().getTime();
                        this.timerId = setInterval
                                (
                                        function () {
                                                var volumeLevel = me.getVolumeLevel();
                                                var now = new Date().getTime();
                                                var distance = now - me.countDownDate;
                                                me.callbackTable[CB_updateRecorderProgress](me.callback, me.deltaTime + distance, volumeLevel);

                                        },
                                        this.subscriptionDuration
                                );
                }
        }

        stopTimer() {
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:---> stopTimer()');
                if (this.timerId != null) {
                        clearInterval(this.timerId);
                        var now = new Date().getTime();
                        var distance = now - this.countDownDate;
                        this.deltaTime += distance;
                        this.timerId = null;
                }
                this.callbackTable[CB_recorder_log](this.callback, DBG, 'JS:<--- stopTimer()');
        }

}

