define(['jquery', 'core/log', 'core/notification', 'core/ajax', 'mod_minilesson/ttaudiohelper', 'mod_minilesson/ttbrowserrec',
    'core/str', 'mod_minilesson/timer', 'mod_minilesson/ttmsspeech', 'mod_minilesson/mediauploader'],
    function ($, log, notification, ajax, audioHelper, browserRec, str, timer, msspeech, mediauploader) {
        "use strict"; // jshint ;_;
        /*
        *  The TT recorder
         */

        log.debug('TT Recorder: initialising');

        return {
            waveHeight: 75,
            audio: {
                stream: null,
                blob: null,
                dataURI: null,
                start: null,
                end: null,
                isRecording: false,
                isRecognizing: false,
                isWaiting: false,
                transcript: null
            },
            submitting: false,
            owner: '',
            controls: {},
            uniqueid: null,
            audio_updated: null,
            maxtime: 0,
            passagehash: null,
            region: null,
            asrurl: null,
            lang: null,
            browserrec: null,
            usebrowserrec: false,
            currentTime: 0,
            stt_guided: false,
            currentPrompt: false,
            speechtoken: '',
            speechtokenvalidseconds: '',
            speechtokentype: '',
            forcestreaming: false,
            is_streaming: false,
            using_msspeech: false,
            msspeech_instance: null,
            savemedia: false,
            uploader: null,
            strings: {},

            //for making multiple instances
            clone: function () {
                return $.extend(true, {}, this);
            },

            init: function (opts) {

                var that = this;
                this.uniqueid = opts['uniqueid'];
                this.callback = opts['callback'];
                this.stt_guided = opts['stt_guided'] ? opts['stt_guided'] : false;
                this.init_strings();
                this.prepare_html();
                this.controls.recordercontainer.show();
                this.register_events();

                //init media uploader
                log.debug('ttr uploader savemedia: ' + this.savemedia);
                if (this.savemedia) {
                    log.debug('ttr uploader creating: ');
                    that.uploader = mediauploader.clone();
                    var uconfig = {};
                    uconfig.wstoken = that.wstoken;
                    uconfig.moodlewsrestformat = "";
                    uconfig.mediatype = that.mediatype;
                    uconfig.parent = that.wwwroot;
                    uconfig.appid = that.appid;
                    uconfig.owner = that.owner;
                    uconfig.region = that.savemediaregion;
                    uconfig.expiredays = that.expiredays;
                    uconfig.transcode = that.transcode ? 1 : 0;
                    uconfig.cloudpoodllurl = that.cloudpoodllurl;
                    uconfig.transcoder = "default";
                    uconfig.transcribe = 0;
                    uconfig.subtitle = 0;
                    uconfig.language = that.lang;
                    uconfig.transcribevocab = "none";
                    uconfig.notificationurl = "none";
                    that.uploader.init(uconfig);
                }

                //token check
                this.using_msspeech = this.can_msspeech();
                if (this.using_msspeech) {
                    var referencetext = opts['referencetext'];
                    this.msspeech_instance = msspeech.clone();
                    this.msspeech_instance.init(this.speechtoken, this.region, this.lang, referencetext);
                }

                // Callback: Timer updates.
                var handle_timer_update = function () {
                    var displaytime = that.timer.fetch_display_time();
                    that.controls.timerstatus.html(displaytime);
                    log.debug('timer_seconds: ' + that.timer.seconds);
                    log.debug('displaytime: ' + displaytime);
                    if (that.timer.seconds == 0 && that.timer.initseconds > 0) {
                        that.update_audio('isRecognizing', true);
                        if (that.usebrowserrec) {
                            that.browserrec.stop();
                        } else {
                            that.audiohelper.stop();
                        }
                    }
                };

                // Callback: Recorder device errors.
                var on_error = function (error) {
                    switch (error.name) {
                        case 'PermissionDeniedError':
                        case 'NotAllowedError':
                            notification.alert("Error", that.strings.allowmicaccess, "OK");
                            break;
                        case 'DevicesNotFoundError':
                        case 'NotFoundError':
                            notification.alert("Error", that.strings.nomicdetected, "OK");
                            break;
                        default:
                            //other errors, like from Edge can fire repeatedly so a notification is not a good idea
                            //notification.alert("Error", error.name, "OK");
                            log.debug("Error", error.name);
                    }
                };

                // Callback: Recording stopped.
                var on_stopped = function (blob) {
                    that.timer.stop()

                    //if the blob is undefined then the user is super clicking or something
                    if (blob === undefined) {
                        return;
                    }

                    //Update our current audio object
                    var newaudio = {
                        blob: blob,
                        dataURI: URL.createObjectURL(blob),
                        end: new Date(),
                        isRecording: false,
                        length: Math.round((that.audio.end - that.audio.start) / 1000),
                    };
                    that.update_audio(newaudio);

                    // If we have a blob and we need to upload it, do so
                    if (that.savemedia) {
                        that.uploader.uploadBlob(that.audio.blob, 'audio/wav');
                        var message = {};
                        message.type = 'mediasaved';
                        message.mediaurl = that.uploader.config.s3root + that.uploader.config.s3filename;
                        message.bloburl = newaudio.dataURI;
                        log.debug('ttr uploader: callback mediasaved');
                        log.debug(message);
                        that.callback(message);
                    }

                    //  If we are browser rec, we should exit here
                    if (that.usebrowserrec) {
                        return;
                    }

                    //if we are not streaming then upload_transcribe (ie send to poodll servers)
                    if (!that.is_streaming) {
                        if (that.using_msspeech) {
                            that.do_msspeech(that.audio.blob, function (response) {
                                that.gotMSResults(response);
                                that.update_audio('isRecognizing', false);
                            });
                        } else {
                            that.upload_transcribe(that.audio.blob, function (response) {
                                log.debug(response);
                                if (response.data.result === "success" && response.data.transcript) {
                                    that.gotRecognition(response.data.transcript.trim());
                                } else {
                                    notification.alert("Information", that.strings.speechnotrecognized, "OK");
                                }
                                that.update_audio('isRecognizing', false);
                            });
                        }
                    }

                };

                // Callback: Recorder device got stream - start recording
                var on_gotstream = function (stream) {
                    var newaudio = { stream: stream, isRecording: true, isWaiting: false };
                    that.update_audio(newaudio);
                };

                //If browser rec (Chrome Speech Rec) 
                if (browserRec.will_work_ok() && !this.stt_guided && !this.forcestreaming && !this.using_msspeech) {
                    //Init browserrec
                    log.debug("using browser rec");
                    this.browserrec = browserRec.clone();
                    this.browserrec.init(this.lang, this.waveHeight, this.uniqueid);
                    this.usebrowserrec = true;

                    //set up events
                    that.browserrec.onerror = on_error;
                    that.browserrec.onStop = on_stopped;
                    that.browserrec.onend = function () {
                        //do something here
                    };
                    that.browserrec.onstart = function () {
                        //do something here
                    };
                    that.browserrec.onfinalspeechcapture = function (speechtext) {
                        that.gotRecognition(speechtext);
                        that.update_audio('isRecording', false);
                        that.update_audio('isRecognizing', false);
                    };

                    that.browserrec.oninterimspeechcapture = function (speechtext) {
                        that.gotInterimRecognition(speechtext);
                    };


                    //If we have a streaming token
                } else if (this.can_stream() && !this.stt_guided) {
                    this.is_streaming = true;
                    //Init streaming audio helper
                    log.debug("using audio helper and streaming rec");
                    this.audiohelper = audioHelper.clone();
                    this.audiohelper.init(this.waveHeight, this.uniqueid, this);

                    that.audiohelper.onError = on_error;
                    that.audiohelper.onStop = on_stopped;
                    that.audiohelper.onStream = on_gotstream;
                    that.audiohelper.onfinalspeechcapture = function (speechtext) {
                        that.gotRecognition(speechtext);
                        that.update_audio('isRecording', false);
                        that.update_audio('isRecognizing', false);
                    };
                    that.audiohelper.oninterimspeechcapture = function (speechtext) {
                        that.gotInterimRecognition(speechtext);
                    };

                    //If upload_transcriber
                } else {
                    //set up upload_transcriber
                    log.debug("using upload_transcriber");
                    this.audiohelper = audioHelper.clone();
                    this.audiohelper.init(this.waveHeight, this.uniqueid, this);

                    that.audiohelper.onError = on_error;
                    that.audiohelper.onStop = on_stopped;
                    that.audiohelper.onStream = on_gotstream;

                }//end of setting up recorders

                // Set up token refresh
                log.debug('original speechtoken - ' + this.speechtoken);
                log.debug('speechtokentype - ' + this.speechtokentype);
                log.debug('speechtokenvalidseconds - ' + this.speechtokenvalidseconds);
                this.init_token_refresh();

                // Setting up timer.
                this.timer = timer.clone();
                this.timer.init(this.maxtime, handle_timer_update);
                // Init the timer readout
                handle_timer_update();
            },

            can_stream: function () {
                return (this.speechtoken && this.speechtoken !== 'false' && (this.speechtokentype === 'assemblyai' || this.speechtokentype === 'iflytek' || this.speechtokentype === 'azure') && !this.stt_guided);
            },

            can_msspeech: function () {
                return (this.speechtoken && this.speechtoken !== 'false' && this.speechtokentype === 'msspeech');
            },

            update_currentprompt: function (targettext) {
                this.currentPrompt = targettext;
                if (this.using_msspeech) {
                    this.msspeech_instance.set_reference_text(targettext);
                }
            },

            blobToArrayBuffer: function (blob) {
                return new Promise((resolve, reject) => {
                    const reader = new FileReader();
                    reader.onload = function (event) {
                        resolve(event.target.result);
                    };
                    reader.onerror = function (error) {
                        reject(error);
                    };
                    reader.readAsArrayBuffer(blob);
                });
            },

            init_strings: function () {
                var that = this;
                str.get_strings([
                    { "key": "allowmicaccess", "component": 'mod_minilesson' },
                    { "key": "nomicdetected", "component": 'mod_minilesson' },
                    { "key": "speechnotrecognized", "component": 'mod_minilesson' },

                ]).done(function (s) {
                    var i = 0;
                    that.strings.allowmicaccess = s[i++];
                    that.strings.nomicdetected = s[i++];
                    that.strings.speechnotrecognized = s[i++];
                });
            },

            prepare_html: function () {
                this.controls.recordercontainer = $('#ttrec_container_' + this.uniqueid);
                this.controls.recorderbutton = $('#' + this.uniqueid + '_recorderdiv');
                this.controls.waveform = $('#' + this.uniqueid + '_waveform');
                this.controls.timerstatus = $('.timerstatus_' + this.uniqueid);
                this.passagehash = this.controls.recorderbutton.data('passagehash');
                this.region = this.controls.recorderbutton.data('region');
                this.lang = this.controls.recorderbutton.data('lang');
                this.asrurl = this.controls.recorderbutton.data('asrurl');
                this.speechtoken = this.controls.recorderbutton.data('speechtoken');
                this.speechtokenvalidseconds = this.controls.recorderbutton.data('speechtokenvalidseconds');
                this.speechtokentype = this.controls.recorderbutton.data('speechtokentype');
                this.forcestreaming = this.controls.recorderbutton.data('forcestreaming');
                this.maxtime = this.controls.recorderbutton.data('maxtime');
                this.waveHeight = this.controls.recorderbutton.data('waveheight');
                // Whether to save media and related
                this.savemedia = this.controls.recorderbutton.data('savemedia') === 1;
                this.savemediaregion = this.controls.recorderbutton.data('savemediaregion');
                this.wstoken = this.controls.recorderbutton.data('wstoken');
                this.wwwroot = this.controls.recorderbutton.data('wwwroot');
                this.appid = this.controls.recorderbutton.data('appid');
                this.owner = this.controls.recorderbutton.data('owner');
                this.transcode = this.controls.recorderbutton.data('transcode') === 1;
                this.expiredays = this.controls.recorderbutton.data('expiredays');
                this.mediatype = this.controls.recorderbutton.data('mediatype');
                this.cloudpoodllurl = this.controls.recorderbutton.data('cloudpoodllurl');

            },

            init_token_refresh: function () {
                var that = this;
                var validsecs = Number(that.speechtokenvalidseconds) || 0;
                // If we have a token, then we can set up a timer to refresh it
                if (that.speechtoken && validsecs > 0) {
                    //(valid until seconds - now seconds) => milliseconds
                    var refreshInterval = validsecs * 1000;
                    log.debug('Refreshing ' + that.speechtokentype + ' token after ' + refreshInterval + ' milliseconds');
                    if (refreshInterval > 0) {
                        setTimeout(function () {
                            // Refresh the token
                            log.debug('Refreshing streaming token...');
                            var ajaxresult = ajax.call([{
                                methodname: 'mod_minilesson_refresh_token',
                                args: {
                                    'type': that.speechtokentype,
                                    'region': that.region
                                },
                                async: false
                            }])[0].then(ajaxresult => {
                                log.debug('New token ajaxresult:', ajaxresult);
                                var newtoken = JSON.parse(ajaxresult);
                                log.debug('New token received:', newtoken);
                                // Update our internal token
                                if (newtoken && newtoken.token) {
                                    that.speechtoken = newtoken.token;
                                    that.speechtokenvalidseconds = newtoken.validseconds;
                                    switch (that.speechtokentype) {
                                        case 'assemblyai':
                                            that.helper.streamer.updatetoken(newtoken.token);
                                            break;
                                        case 'iflytek':
                                            that.helper.streamer.updatetoken(newtoken.token);
                                            break;
                                        case 'azure':
                                            that.helper.streamer.updatetoken(newtoken.token);
                                            break;
                                        case 'msspeech':
                                            that.msspeech_instance.updatetoken(newtoken.token);
                                            break;
                                    }
                                    log.debug('Streaming token refreshed successfully.');
                                    // Start the countdown all over again
                                    that.init_token_refresh();
                                } else {
                                    log.debug('New token was not a token.');
                                }
                            });

                        }, refreshInterval);

                    } else {
                        log.debug('Refresh interval is 0. Not refreshing token.');
                    }
                } else {
                    log.debug('No valid streaming token available, skipping refresh setup.');
                }
            },

            silence_detected: function () {
                if (this.audio.isRecording) {
                    this.toggleRecording();
                }
            },

            update_audio: function (newprops, val) {
                if (typeof newprops === 'string') {
                    log.debug('update_audio:' + newprops + ':' + val);
                    if (this.audio[newprops] !== val) {
                        this.audio[newprops] = val;
                        this.audio_updated();
                    }
                } else {
                    for (var theprop in newprops) {
                        this.audio[theprop] = newprops[theprop];
                        log.debug('update_audio:' + theprop + ':' + newprops[theprop]);
                    }
                    this.audio_updated();
                }
            },

            register_events: function () {
                var that = this;
                this.controls.recordercontainer.click(function () {
                    that.toggleRecording();
                });

                this.audio_updated = function () {
                    //pointer
                    if (that.audio.isRecognizing || that.audio.isWaiting) {
                        that.show_recorder_pointer('none');
                    } else {
                        that.show_recorder_pointer('auto');
                    }
                    //the color
                    //we no longer swap out colors for waiting .. its too fast and a bit jarring
                    if (that.audio.isRecognizing || that.audio.isWaiting) {
                        this.controls.recorderbutton.removeClass('ttrec_ready');
                        this.controls.recorderbutton.removeClass('ttrec_waiting');
                        this.controls.waveform.removeClass('ttrec_waiting');
                        this.controls.recorderbutton.removeClass('ttrec_isrecording');
                        this.controls.recorderbutton.addClass('ttrec_engaged');
                    } else if (that.audio.isRecording) {
                        this.controls.recorderbutton.removeClass('ttrec_ready');
                        this.controls.waveform.removeClass('ttrec_waiting');
                        this.controls.recorderbutton.removeClass('ttrec_waiting');
                        this.controls.recorderbutton.removeClass('ttrec_engaged');
                        this.controls.recorderbutton.addClass('ttrec_isrecording');
                    } else if (that.audio.isWaiting && false) {
                        this.controls.recorderbutton.removeClass('ttrec_engaged');
                        this.controls.recorderbutton.removeClass('ttrec_ready');
                        this.controls.recorderbutton.removeClass('ttrec_isrecording');
                        this.controls.recorderbutton.addClass('ttrec_waiting');
                        this.controls.waveform.addClass('ttrec_waiting');
                    } else {
                        this.controls.recorderbutton.removeClass('ttrec_engaged');
                        this.controls.recorderbutton.removeClass('ttrec_waiting');
                        this.controls.recorderbutton.removeClass('ttrec_isrecording');
                        this.controls.waveform.removeClass('ttrec_waiting');
                        this.controls.recorderbutton.addClass('ttrec_ready');
                    }

                    //the font awesome spinner/mic/square
                    that.controls.recorderbutton.html(that.recordBtnContent());
                };

            },

            show_recorder_pointer: function (show) {
                if (show) {
                    this.controls.recorderbutton.css('pointer-events', 'none');
                } else {
                    this.controls.recorderbutton.css('pointer-events', 'auto');
                }

            },

            startedRecording: function () {

                var message = {};
                message.type = 'recording';
                message.results = '';
                this.callback(message);
            },

            gotMSResults: function (results) {
                log.debug(results);
                var message = {};
                message.type = 'pronunciation_results';
                message.results = results;
                this.callback(message);
            },

            gotRecognition: function (transcript) {
                log.debug('transcript:' + transcript);
                if (transcript.trim() == '') { return; }
                var message = {};
                message.type = 'speech';
                message.capturedspeech = transcript;
                this.callback(message);
            },

            gotInterimRecognition: function (transcript) {
                var message = {};
                message.type = 'interimspeech';
                message.capturedspeech = transcript;
                //POINT
                this.callback(message);
            },

            cleanWord: function (word) {
                return word.replace(/['!"#$%&\\'()\*+,\-\.\/:;<=>?@\[\\\]\^_`{|}~']/g, "").toLowerCase();
            },

            recordBtnContent: function () {

                if (!this.audio.isRecognizing) {

                    if (this.audio.isRecording) {
                        return '<i class="fa fa-stop">';

                    } else if (this.audio.isWaiting) {
                        return '<i class="fa fa-solid fa-cog fa-spin">';

                    } else {
                        return '<i class="fa fa-microphone">';
                    }
                } else {
                    return '<i class="fa fa-spinner fa-spin">';
                }
            },
            toggleRecording: function () {
                var that = this;

                //If we are recognizing, then we want to discourage super click'ers
                if (this.audio.isRecognizing || this.audio.isWaiting) {
                    return;
                }

                //If we are currently recording
                if (this.audio.isRecording) {
                    that.timer.stop();

                    //If using Browser Rec (chrome speech)
                    if (this.usebrowserrec) {
                        that.update_audio('isRecording', false);
                        that.update_audio('isRecognizing', true);
                        this.browserrec.stop();

                        //If using upload_transcriber or streaming
                    } else {
                        this.update_audio('isRecognizing', true);
                        this.audiohelper.stop();
                    }

                    //If we are NOT currently recording
                } else {
                    // Run the timer
                    that.currentTime = 0;
                    that.timer.reset();
                    that.timer.start();


                    //If using Browser Rec (chrome speech)
                    if (this.usebrowserrec) {
                        this.update_audio('isRecording', true);
                        this.browserrec.start();

                        //If using Audio helper for upload_transcriber or streaming
                    } else {
                        var newaudio = {
                            stream: null,
                            blob: null,
                            dataURI: null,
                            start: new Date(),
                            end: null,
                            isRecording: false,
                            isRecognizing: false,
                            isWaiting: true,
                            transcript: null
                        };
                        this.update_audio(newaudio);
                        this.audiohelper.start();
                    }
                    this.startedRecording();
                }
            },

            upload_transcribe: function (blob, callback) {
                var bodyFormData = new FormData();
                var blobname = this.uniqueid + Math.floor(Math.random() * 100) + '.wav';
                bodyFormData.append('audioFile', blob, blobname);
                bodyFormData.append('scorer', this.passagehash);
                if (this.stt_guided) {
                    bodyFormData.append('strictmode', 'false');
                } else {
                    bodyFormData.append('strictmode', 'true');
                }
                //prompt is used by whisper and other transcibers down the line
                if (this.currentPrompt !== false) {
                    bodyFormData.append('prompt', this.currentPrompt);
                }
                bodyFormData.append('lang', this.lang);
                bodyFormData.append('wwwroot', M.cfg.wwwroot);

                var oReq = new XMLHttpRequest();
                oReq.open("POST", this.asrurl, true);
                oReq.onUploadProgress = function (progressEvent) { };
                oReq.onload = function (oEvent) {
                    if (oReq.status === 200) {
                        callback(JSON.parse(oReq.response));
                    } else {
                        callback({ data: { result: "error" } });
                        log.debug(oReq.error);
                    }
                };
                try {
                    oReq.send(bodyFormData);
                } catch (err) {
                    callback({ data: { result: "error" } });
                    log.debug(err);
                }
            },

            do_msspeech: function (blob, callback) {
                this.msspeech_instance.recognize(blob, callback)
            },

        };//end of return value

    });