declare var module: any;
declare var define :any;
let w: any = this;
let d: any = w.document;
const a: string = 'audio';
const v: string = 'video';
const s: string = 'script';

"use strict";
class PlatformDetector {
	private regex: Object = {
		platform: {
            playstation: new RegExp('^Mozilla/[0-9].[0-9]\\s.(PlayStation [3-5]).*\\sSCEE/[1-9].[0-9]\\sNuanti/[1-9].[0-9]'),
			mobile: new RegExp('Mobile|Android|Tab|Tablet|GT|SM-(T|P)|Silk-Accelerated', 'i'),
			tablet: new RegExp('Tab|Tablet|iPad|SM-(T|P)|GT|Silk-Accelerated', 'i'),
			desktop: new RegExp('(?:(?!Mobile|Android|Tab|Tablet|GT|SM-(T|P)|Silk-Accelerated).)', 'i')
		},
		os: {
			android: new RegExp('Linux.*Android'),
			ios: new RegExp('like\\sMac\\sOS\\sX'),
			mac: new RegExp('Intel\\sMac\\sOS\\sX'),
			windows: new RegExp('Windows\\sNT')
		},
		browser: {
            ie: new RegExp('msie|trident', 'i'),
            edge: new RegExp('Edge'),
            chrome: new RegExp('Chrome'),
            firefox: new RegExp('Firefox'),
            safari: new RegExp('Safari')
		}
	};
	private canInit: boolean = false;

	constructor() {
		if (w.navigator && w.navigator.userAgent && w['RegExp'] && typeof w['RegExp'] === 'function') {
			this.canInit = true;
		} else {
			this.throwErr();
		}
	}

	public detectDevice(): Object {
		return {'platform': this.detect('platform'), 'os': this.detect('os'), 'browser': this.detect('browser')};
	}

	public detect(type): string {
		if (this.canInit) {
			let obj = this.regex[type];
			let regxName: string;
			for (regxName in obj) {
				if (obj[regxName].test(w.navigator.userAgent)) {
					return regxName;
				}
			}
		} else {
			this.throwErr();
		}
	}

	private throwErr(): void {
		if (w['Error'] && typeof w['Error'] === 'function') {
			throw new Error("This browser doesn't supprot platform detection.");
		} else {
			console.error("This browser doesn't supprot platform detection.");
		}
	}
}
interface GalacticInterface {
	list: any;
	shuffleList: any;
	playNextList: Array<any>;
	currentIndex: number;
	shuffleIndex: number;
	options: any;
	currentMediaItem: Object;
	audioElement: HTMLAudioElement
	//audioSourceElement: HTMLSourceElement;
	currentSrc: string;
	audioElementEvents: {};
	progress: number;
    position: number;
    remainingTime: number;
    lastPosition: number;
    bufferProgress: number;
    lastBufferProgress: number;
    audioTrackPlaying: boolean
    audioTrackOnload: boolean;
    errorTimeout: any;
    interval: any;
    error: boolean;
    metadata: boolean;
    isRepeating: boolean;
    isShuffling: boolean;
    isPaused: boolean;
    eventMap: Object;
    duration: number;
    isReloading: boolean;
    mimeRegx: RegExp;
    device: any;
    windows: boolean;
    playstation: boolean;
    onlinecbreload: boolean;
    playState: boolean;
    emptyWaveFileBase64: string;
    emptyVideoFileBase64: string;
    playPromise: any;
    forceDurationChangeForAudioAd: boolean;
    script: HTMLScriptElement;
    hasHlsScript: HTMLScriptElement;
    forcePause: boolean;
    setup(options?: any): any;
    init(): any;
    destroy(): any;
    isValid(): boolean;
    load(withSrc?: any, afterReload?: boolean): any;
    reload(time: number): any;
    onBufferChange(e: Event): void;
    onDurationChange(e: Event): void;
  	play(): any;
  	pause(): any;
  	stop(callback?: Function): any;
	next(prop: string, callback?: Function): any;
    previous(prop: string, callback?: Function): any;
    repeat(callback?: Function): any;
    flush(): any;
    loadAudio(): any;
    setSrc(src: string): any;
  	setPosition(position: number): any;
    setTimeValues(progress: number, remainingTime: number, position: number): void;
    setVolume(volume: number): any;
  	dispatchEvent(type: string, args: Object): void;
    catchErrorVulnerability(e: Event): void;
    setNextPlayingListItem(mediaItem: Object): any;
    getNextPlayingListItem(): Object;
    removeNextPlayingListItem(): any;
    hasNextPlayingListItem(): boolean;
    applyNextAction(): any;
    on(type: string, callback: Function): void;
	off(type: string, filler: any): void; 
	shuffle(callback?: Function): any;
	createShuffleList(list: Array<any>): Array<any>;
	toggle(prop: string): any;
	isValid(): boolean;
	isBufferValid(): boolean;
	hasBuffer(): boolean;
	waitUntilOnline(): any;
	clearWaiters(): any;
	setAttribute(type: string, value: string): any;
	loadEmpty(): any;
	addToPlaylist(data: any, type?: string, callback?: Function): any;
    createElement (parent: HTMLElement, type: string): any;
    addEventListeners (element: HTMLElement, events: Object): any;
    setOptions(options: Object): any;
    //createMediaHelper(): void;
}

class Galactic implements GalacticInterface {

	"use strict";

    /*
     * Media Item & Lists
     */
    public currentMediaItem: Object;
	public list: any;
	public shuffleList: any;
	public playNextList: Array<any>;

    /*
     * Main Used Indexes 
     */
	public currentIndex: number;
	public shuffleIndex: number;

    /*
     * Main Elements
     */
	public audioElement: HTMLAudioElement;
	//public audioSourceElement: HTMLSourceElement;


    /*
     * Audio Source URL
     */
	public currentSrc: string;


    /*
     * Main Events Objects
     */
	public audioElementEvents: Object;
    public eventMap: Object;

    /*
     * Main Options
     */
	public options: any;


    /*
     * To Handle the case of loading for mobile and handle all mobile browsers exceptions & warnings
     */
	public loadEmpty: any;
	private emptyWaveFileBase64: string;


    /*
     * Main Regex for to check for audio Type
     */
	private mimeRegx: RegExp;


    /*
     * Audio State Flags
     */
    private playState: boolean;
	public audioTrackPlaying: boolean;
	private audioTrackOnload: boolean;
    public isRepeating: boolean;
    public isShuffling: boolean;
    public isPaused: boolean;
    private isReloading: boolean;
    private metadata: boolean;


    /* 
     * Handle Error and interval for reloading and on online callback
     */
	private errorTimeout: any;
	private interval :any;
	private onlinecbreload: boolean;
    private online: boolean;
    private error: boolean;

	

    /*
     * Devices & Platfornms Falgs
     */
	private device: any;
	private windows: boolean;
	private playstation: boolean;

    /*
     * Handle Data About Time, Duration, Position, etc
     */
	private progress: number;
    private position: number;
    private remainingTime: number;
    private bufferProgress: number;
    private lastBufferProgress: number;
    private lastPosition: number;
    private duration: number;

  

    /*
     * This promise object that is retuned when the browser has safely played the media element
     * It is mainly used to play() pause() load() safely and to handle exceptions thrown by the browser
     */
    private playPromise: any;


    /*
     * Audio Ads Data Management
     * Used to handle sending duration for the audio ad of type m4a 
     *************************** 
      TO-DO: check this
     ***************************
     */
    public forceDurationChangeForAudioAd: boolean;


    private hlsjsErrorHandler: any;
    private hlsVideoTagErrorHandler: any;
    private isUsingHLSPolyfill: boolean;
    private hls: any;
    private script: HTMLScriptElement;
    private hasHlsScript: HTMLScriptElement;
    private tempElRef: HTMLElement;
    private emptyVideoFileBase64: string;
    private forcePause: boolean;

    constructor() {
    	this.setup();
    }

    /****************************************************************************************************************************
     * Boilerplate: set default values 
     */
    private setup(): any {  
    	//Main Audio Events
    	this.audioElementEvents = {
            abort: (e) => {
               console.log('The abort event occurs when the loading of an audio/video is aborted. Sent when playback is aborted; for example, if the media is playing and is restarted from the beginning, this event is sent.');
                this.checkError(e);
            },
            stalled: (e) => {
                console.log('browser is trying to get media data, but data is not available');
                this.checkError(e); //when it's not paused
            },
            suspend: (e) => {
                console.log('loading of the media is suspended (prevented from continuing). This can happen when the download has completed, or because it has been paused for some reason.');
                if (this.bufferProgress <= 98) {
                    this.checkError(e);
                }
            },
            error: (e) => {
                console.log('an error occurred during the loading of an audio error: ' + e.target.error.code + ' ' + e.target.error.message);
                this.checkError(e);

                if (this.options.video && this.isUsingHLSPolyfill) {
                    const mediaError: any = e.currentTarget.error;
                    if (mediaError.code === mediaError.MEDIA_ERR_DECODE) {
                        this.hlsVideoTagErrorHandler();
                    }
                }
            },
            emptied: (e) => {
                // *** TO DO *** // check for error in this state
            	this.audioTrackPlaying = undefined;
            },
            waiting: (e) => {
                this.audioTrackPlaying = false;
                console.log('audio stops because it needs to buffer the next frame.');
                this.checkError(e);
                this.hasBuffer() && this.onBufferChange(e); 
                this.dispatchEvent('wait', {status: this.audioTrackPlaying});
            },
            loadedmetadata: (e) => {
                console.log('loaded meta data for audio/video consists of: duration, dimensions (video only) and text tracks.');
            	this.metadata = true;
            },   
            loadstart: (e) => {
                this.audioTrackPlaying = false;
                console.log('the browser is currently looking for the specified audio/video');
            	this.dispatchEvent('wait', {status: this.audioTrackPlaying});
                this.hasBuffer() && this.onBufferChange(e); 
            },
            durationchange: (e) => {
                // if (this.forcePause && !this.audioElement.paused) {
                //     this.pause(true);
                // }
                console.log('when an audio/video is loaded, the duration will change from "NaN" to the actual duration of the audio/video');
                this.isValid() && this.onDurationChange(e);

		    	if (this.lastPosition) {
		    		this.setPosition(this.lastPosition);
		    		this.lastPosition = undefined;
		    	}

		    	this.duration = e.target.duration;
            },
            timeupdate: (e) => {
                // if (this.forcePause && !this.audioElement.paused) {
                //   this.pause(true);
                // }
                // if (this.progress > 3 && this.progress < 5 && this.audioElement.muted) {
                //     console.log('UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING UNMUTING', this.audioElement.volume, this.audioElement.muted, this.progress, this);

                //    try { this.removeAttribute('muted')
                //         .setAttribute('muted', 'false')
                //         .setVolume(1);

                //     this.tempElRef = d.getElementsByTagName(v)[0];
                //     this.tempElRef.muted = false;
                //     this.tempElRef.play();
                //     delete this.tempElRef;
                // } catch(e){}
                // }
                // if (this.options.video && this.audioElement.muted) {
                    

                //     // this.removeAttribute('muted')
                //     //     .setAttribute('muted', 'false')
                //     //     .setVolume(1);
                //     //     this.tempElRef = d.getElementsByTagName(v)[0];
                //     // this.tempElRef.muted = false;
                //     // delete this.tempElRef;
                // }
                console.log('playing position of an audio/video has changed.', this);
                this.playState = true;
                if (this.isValid()) {
                	this.onDurationChange(e);
                	this.audioTrackPlaying = true;
                }

                this.hasBuffer() && this.onBufferChange(e);

                if (this.isReloading) {
                	this.isReloading = this.error = false;
                }

                this.bufferProgress === 100 && this.clearWaiters(); //kill waiters in case it is fully downloaded
            },
            canplay: (e) => {
                if (this.forcePause && !this.audioElement.paused) {
                    this.pause(undefined, true);
                  }
                console.log('can start playing the audio/video, enough has loaded to play...');
            	this.dispatchEvent('readyToPlay', {track: this.currentMediaItem, src: this.currentSrc, progress: this.progress});
                
                if (this.options.playOnLoad) {
                    // alert('play from can play');
                    this.play();
                }

                // if (!this.options.video) {
                //     // this.audioTrackPlaying = true;
                //     console.log('playing from canplay');
                //     this.play();
                // }
            },
            playing: (e) => {
                if (this.forcePause && !this.audioElement.paused) {
                    this.pause(undefined, true);
                  }
                console.log('audio is playing after having been paused or stopped for buffering');
	        	//this.audioTrackPlaying = true;
	            this.isValid() && this.dispatchEvent('play', {status: this.audioTrackPlaying});
	            this.hasBuffer() && this.onBufferChange(e); 
            },
            pause: (e) => {
                this.forcePause = false;
                this.isPaused = true;
                this.audioTrackPlaying = false;
                this.isValid() && this.dispatchEvent('pause', {status: this.audioTrackPlaying});
            },
            ended: (e) => {
                this.isValid() && this.dispatchEvent('end', {progress: this.progress});
            },
            progress: (e) => {
                console.log('downloading the specified audio/video... ');
                this.hasBuffer() && this.onBufferChange(e);  
            },    
            ratechange: (e) => {
                this.isValid() && this.dispatchEvent('rateChange');
            },
            volumechange: (e) => {
                this.isValid() && this.dispatchEvent('volumeChange');
            },
            seeking: (e) => {
            	this.pause();
            },
            seeked: (e) => {
                if (this.options.playOnLoad) {
                    this.play(); 
                }
            }   
            // loadeddata: (e) => {
            //     console.log('data for the current frame is loaded, but not enough data to play next frame of the specified audio/video ');
            // },
            // play: (e) => {
            //     console.log('audio has been started or is no longer paused');
            // },
            // canplaythrough: (e) => {
            //     console.log('enough has loaded to play without buffering...');
            // },
        };

        //Default options
    	this.options = { 
    		html5: false, 
    		reloadOnInit: 10000, 
    		reloadOnError: 5000, 
            volume: 1,
            fadeIn: false,
            hls: false
    	};   
        this.emptyVideoFileBase64 = 'data:application/x-mpegURL;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';
    	this.emptyWaveFileBase64 = 'data:audio/wave;base64,UklGRjIAAABXQVZFZm10IBIAAAABAAEAQB8AAEAfAAABAAgAAABmYWN0BAAAAAAAAABkYXRhAAAAAA==';
    	this.mimeRegx = new RegExp(/\.m4a|mp4|mpeg4|aac|flv|mov|m4v|f4v|.m4b|mp4v|3gp|3g2|mp4|m3u8/);

    	//Objects & arrays
    	this.eventMap = {};
    	this.list = this.playNextList = this.shuffleList = []; 
    	this.currentMediaItem = {};

    	//Default values
    	this.online = true;
    	this.bufferProgress = this.progress = this.lastBufferProgress = this.position = this.remainingTime = this.currentIndex = this.shuffleIndex = this.duration = 0; 
    	this.audioElement = this.errorTimeout = this.lastPosition = this.error = this.metadata = this.isRepeating = this.isShuffling = this.isPaused = this.isReloading = this.interval = this.onlinecbreload = this.playState = this.forceDurationChangeForAudioAd = this.playPromise = undefined;
    	
    	//Device detections
    	this.device = new PlatformDetector().detectDevice();
    	this.windows = (this.device.browser === 'ie'); //(this.device.os === 'windows');
    	this.playstation = (this.device.platform === 'playstation');

    	//Load empty for mobile browsers
    	this.loadEmpty = (() => {
        	return () => {
                this.forcePause = false;
	        	this.load(false);//.play();
	        	//return this;
        	};
        })();

        if (!this.windows && !this.playstation) {
    		w.addEventListener('offline', this.offlinecb, false);
    		w.addEventListener('online', this.onlinecb, false);
    	}
    }
    /****************************************************************************************************************************/



    private appendHLSEvents (): void {
        // update live status on level load
        // this.hls.on(w.Hls.Events.LEVEL_LOADED, function(event, data) {
        //     duration = data.details.live ? Infinity : data.details.totalduration;
        // });
        this.hls.on(w.Hls.Events.MANIFEST_PARSED, () => {
            console.log('playing from manifest');
           if (this.options.playOnLoad) {
                // alert('play from appendHLSEvents');
                this.play();
            }
        });
        // try to recover on fatal errors
        this.hls.on(w.Hls.Events.ERROR, (event: Object, data: any) => {
            if (data.fatal) {
                switch (data.type) {
                    case w.Hls.ErrorTypes.NETWORK_ERROR:
                        this.hls.startLoad();
                        break;
                    case w.Hls.ErrorTypes.MEDIA_ERROR:
                        this.hlsjsErrorHandler();
                        break;
                    default:
                        console.error('Error loading media: File could not be played');
                        this.destoryHLS();
                        break;
                }
            }
        });
    }   

    /****************************************************************************************************************************
     * Main Initializer
     */
    public init(options?: any): any {
        this.setOptions(options);
        this.createMedia();
        
        return this;      
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Connecitivty Manager: Methods Needed
     */
    private isOnline(): boolean {

      // Handle IE and more capable browsers
      let xhr: XMLHttpRequest = new XMLHttpRequest();// (w.XMLHttpRequest)( "Microsoft.XMLHTTP" );

      // Open new request as a HEAD to the root hostname with a random param to bust the cache
      xhr.open('HEAD', `//${w.location.hostname}/?rand=${w.Math.floor((1 + w.Math.random()) * 0x10000)}`, false); // secon argument async :

      // Issue request and handle response
      try {
        xhr.send();
        return (xhr.status >= 200 && (xhr.status < 300 || xhr.status === 304));
      } catch (error) {
        return false;
      }
    }

    private waitUntilOnline(): any {
        if (this.windows || this.playstation) {
            this.interval = this.interval ? this.interval : w.setInterval(() => {
                this.online = this.isOnline();
                if (this.online) {
                    this.reload(0);
                }  
            }, 2000);
        } else {
            this.onlinecbreload = true;
        }
        return this;
    }

    private onlinecb(): void {
        console.log('is online...onlinecbreload: ', this.onlinecbreload);
        this.online = true;
        this.onlinecbreload && this.reload(0);
    }

    private offlinecb(): void {
        console.log('is offline...');
        this.online = false;
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Buffer Progress Management
     */
   	private onBufferChange(e: any): void {
        try {
         	let duration: number = e.target.duration;
         	let buffered: any = e.target.buffered;
         	let currentTime: number = e.target.currentTime;
         	let i: number = 0;
         	let buffLen: number = buffered.length;
         	this.lastBufferProgress = this.bufferProgress;

    	    if (duration > 0 && buffLen > 0) {
    	        for (; i < buffLen; i++) {
    	            if (buffered.start(buffLen - 1 - i) < currentTime) {
    	                this.bufferProgress = w.Math.ceil((buffered.end(buffLen - 1 - i) / duration) * 100);
    	                break;
    	            }
    	        }
    	    } else { 
    	    	this.bufferProgress = undefined;
    	    }
    	    this.isBufferValid() && this.dispatchEvent('bufferChange', { bufferProgress: this.bufferProgress });
        } 
        catch (e){}
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Duration Management
     */
    private onDurationChange(e: any): void {
    	let d: number = e.target.duration ? e.target.duration*1000 : 0;
  	    let c: number = e.target.currentTime ? e.target.currentTime*1000 : 0;
        let p: number = (c/d)*100;

        //p: progress , d-c: remainingTime, c: position
        this.setTimeValues(p, d-c, c); 
    }

    private setTimeValues(progress: number, remainingTime: number, position: number): void {
        this.progress = progress;
        this.remainingTime = remainingTime;
        this.position = position;
        this.position > 0 && this.dispatchEvent('durationChange', { progress: this.progress, remainingTime: this.remainingTime, position: this.position, duration: this.audioElement.duration, status: this.audioTrackPlaying });
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Seek To A Certain Position 
     */
    public setPosition(position: number): any {
        if (this.playState) {
            try { //for ie
                this.audioElement.currentTime = Number(position) / 1000;
            } catch(e) {}
        }
        return this;
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Reload Management
     */
    private reload(time: number, force?: boolean): any {

        if(this.position > 1000) {
            this.lastPosition = this.position;
        }

        this.errorTimeout = this.errorTimeout ? this.errorTimeout : w.setTimeout(() => {
            if (this.online && !this.audioTrackPlaying && this.error) { 
                if (this.audioElement.readyState >= 2 && this.audioElement.networkState === 2) {
                    this.isReloading = true;
                    this.clearWaiters().play(true); 
                } else {
                    this.load(this.currentSrc, true);
                }
            } else {  
                this.error = false;
                this.isReloading = false;
                this.clearWaiters();
            }
        }, time);
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Volume Management
     */
    public setVolume(volume: number): any { // from 0 to 100
        try {
            if (volume > 1.0) {
                return;
            }
            this.audioElement.volume = volume;
            this.dispatchEvent('volumeChange', {"volume":this.audioElement.volume});
            return this;
        } catch(e) {}
    }

    public getVolume(): number {
    	return this.audioElement.volume;
    }
    /***************************************************************************************************************************/




    /****************************************************************************************************************************
     * Main Audio Functionality: Play, Pause, Shuffle, Next, Previous, Repeat
     */
    private loadAudio(): any {
        if (this.options.video && this.isUsingHLSPolyfill) {
            this.createNewHlsContext();
            this.hls.loadSource(this.currentSrc);
        } else {
            this.audioElement.load();
        }
        return this;
    }

    public load(withSrc?: any, afterReload?: boolean): any {
        //dispatchEvent('wait', {status: this.audioTrackPlaying})
        this.flush(true);
        //if (!this.isUsingHLSPolyfill) {
        afterReload && this.loadAudio();
        //}

        if (withSrc) {
            this.setSrc(withSrc);
            console.log('THE SOURCE IS: ', withSrc);
        } else {
            this.setSrc(this.options.video ? this.emptyVideoFileBase64 : this.emptyWaveFileBase64);
            console.log('THE SOURCE IS: ', this.options.video ? this.emptyVideoFileBase64 : this.emptyWaveFileBase64);
        }

        // if (!this.isUsingHLSPolyfill) {
            this.setAttribute('src', this.currentSrc); //this.options.html5 ? this.audioElement : this.audioSourceElement
       // }
        this.loadAudio();
        return this;
    }

    public fadeIn (start: number, end: number, duration: number, cb?: Function): void {
        end = Math.round(end);
        let delta: number = end - start;
        let startTime: number = null;
        let t: number;
        let factor: number;
        let frame: any;

        this.setVolume(0);

        let tweenLoop: FrameRequestCallback = (time?: number) => {

            if (!time) {
                time = new Date().getTime();
            }
            if (startTime === null) {
              startTime = time;
            }

            t = time - startTime;
            factor = t / duration;

            this.setVolume(start + delta * factor);
            
            if (t < duration && this.getVolume() < end) {  
                console.log('animating ', this.getVolume());
                frame = w.requestAnimationFrame(tweenLoop);
                return;
            }   
            w.cancelAnimationFrame(frame);
            console.log('done animating ', this.getVolume());
            if (cb) {
                cb();
            }
            tweenLoop = delta = startTime = t = time = start = frame = null;   
        };
        frame = w.requestAnimationFrame(tweenLoop);
    }

    public play(): any { 
        this.isPaused = false;
        //this.audioTrackPlaying = undefined;
        // if (!this.playstation && !this.options.video && !this.isReloading) {  //  //  
        //     let isPlayingToPreventRaceCondition: boolean = this.audioElement.currentTime > 0 && !this.audioElement.paused && !this.audioElement.ended && this.audioElement.readyState >= 2;
        //     if (!isPlayingToPreventRaceCondition) {
        //         try {
        //             this.checkFadeIn();
        //             //https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
        //             this.playPromise = this.audioElement.play();
        //             if (this.playPromise) {
        //                 this.playPromise.then(_ => {
        //                     //this.audioTrackPlaying = true;
        //                     // if (this.options.video) {
        //                     //     this.audioElement.play();
        //                     // }
        //                     console.log('should play');
        //                 })
        //                 .catch(error => {});
        //             }
        //         } catch (e) {}
        //     }
        // } 
        // else {
            try {
                this.checkFadeIn();
                this.audioElement.play().catch(e=>{});
            } catch(e) {}
        // }
        return this;
    }

    private checkFadeIn(): void {
        if (this.options.fadeIn) {
            this.fadeIn(0.0, 0.9, 5000);
        }
    }

    public pause(forcePause?: boolean, force?: boolean): any {
        if (forcePause) {
            this.forcePause = true;
        }
       if (this.audioTrackPlaying || force) {
           // if (!this.playstation && !this.options.video && !this.device.browser.match(/safari|edge/)) { //this.device.browser !== 'safari'
           //      if (this.playPromise) {
           //          //https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
           //          this.playPromise.then(_ => {
           //              this.isPaused = true;
           //              this.audioElement.pause();
           //          })
           //          .catch(error => {
           //              this.isPaused = false;
           //          });
           //      }
           //  } 
           //  else {
                this.audioElement.pause();
                // this.isPaused = true;
            //}
        } 
        return this;
    }

    public stop(callback?: Function): any {
        this.pause().flush(true).dispatchEvent('durationChange', { progress: 0, remainingTime: 0, position: 0 });
        this.lastPosition = undefined;
        callback && callback();
        return this;
    }

    public next(prop: string, callback?: Function): any {
        this[prop] = (this[prop] === this.list.length - 1) ? 0 : ++this[prop];
        this.currentMediaItem = this.list[this[prop]];
        callback && callback({'track':this.currentMediaItem});
        return this;
    }

    public previous(prop: string, callback?: Function): any {
        this[prop] = (this[prop] <= 0) ? this.list.length - 1 : --this[prop];
        this.currentMediaItem = this.list[this[prop]];
        callback && callback({'track':this.currentMediaItem});
        return this;
    }

    public repeat(callback?: Function): any {
        !this.isRepeating && this.toggle('repeat');
        this.currentMediaItem = this.list[this.currentIndex];
        callback && callback({'track':this.currentMediaItem});
        this.dispatchEvent('repeatStatusChange', {repeat:this.isRepeating,"track": this.currentMediaItem});
        return this;
    }

    public createShuffleList(list): Array<any> {
        let currentIndex = list.length, temporaryValue, randomIndex;

        // While there remain elements to shuffle...
        while (0 !== currentIndex) {

            // Pick a remaining element...
            randomIndex = Math.floor(Math.random() * currentIndex);
            currentIndex -= 1;

            // And swap it with the current element.
            temporaryValue = list[currentIndex];
            list[currentIndex] = list[randomIndex];
            list[randomIndex] = temporaryValue;
        }

        return list;
    }

    public shuffle(callback?: Function): any {

        !this.isShuffling && this.toggle('shuffle');

        this.shuffleList = (this.shuffleIndex === 0) ? this.createShuffleList(this.list) : this.shuffleList;
        
        if (callback) {
            this.currentMediaItem = this.shuffleList[this.shuffleIndex];
            callback({ "track": this.currentMediaItem, "list": this.shuffleList });
        }
        this.dispatchEvent('shuffleStatusChange', {"shuffle":this.isShuffling,"track": this.currentMediaItem});
        return this;
    }

    public addToPlaylist(data: any, type?: string, callback?: Function): any {
        let copy: Array<any>;
        if (typeof arguments[1] === 'string') {
            if (data.constructor === Array) {
                this[type] = copy = (data.length > 0) ? data : this[type];
            } else {
                this[type].push(data);
                copy = this[type];
            }
        } 
        else {
            if (data.constructor === Array) {
                this.list = this.shuffleList = copy = (data.length > 0) ? data : this.list;
            } else {
                this.list.push(data);
                this.shuffleList = copy = this.list;
            }
        }
        callback && callback({"list":copy});
        this.dispatchEvent('playlistChange', {"playlist":this.list,"shuffleList":this.shuffleList});
        return this;
    }

    public clearPlaylist(): any {
        this.list = this.shuffleList = [];
        return this;
    }

    public setNextPlayingListItem(media: any): any {
        if (media.constructor === Array) {
            this.playNextList = this.playNextList.concat(media);
        } 
        else {
            this.playNextList.push(media);
        }
        return this;
    }

    public getNextPlayingListItem(): Object {
        return this.playNextList[this.playNextList.length - 1];
    }

    // Remove the item played which is the last item
    public removeNextPlayingListItem(): any { 
        // Remove the first item since the queue always plays the first
        this.playNextList.splice(this.playNextList.length - 1, 1); 
        return this;
    }

    public hasNextPlayingListItem(): boolean {
        return (this.playNextList.length > 0) ? true : false;
    }

    public applyNextAction(callback?: Function): any {
        // this.stop();
        if (this.hasNextPlayingListItem()) {
            let call: Function = (mediaItem: Object) => {
                this.removeNextPlayingListItem();
                this.currentMediaItem = mediaItem;
                callback({"track":this.currentMediaItem});
            };
            let track: any = this.getNextPlayingListItem();
            if (this.isShuffling) {
                this.shuffleList.forEach((mediaItem) => {
                    if (mediaItem.id == track.id) {
                        call(mediaItem);
                    }
                });
            } else {
                call(track);
            }
        }
        else if (this.isRepeating) {
            this.repeat(callback);
        } 
        else if (this.isShuffling) {
            this.shuffle().next('shuffleIndex', callback);
        } 
        else {
            this.next('currentIndex', callback);
        }
        return this;
    }

    // This will be called from a click event: since the player always moves forward so that's why we toggle repeat by default
    public applyPreviousAction(callback?: Function): any {
        // this.stop();
        this.isRepeating && this.toggle('repeat', false);
        if (this.isShuffling) {
            this.shuffle().previous('shuffleIndex', callback);
        } 
        else {
            this.previous('currentIndex', callback);
        }
        return this;
    }

    //  Force toggle: toggle('repeat', true|false);
    //  Regular toggle toggles the property and the opposite: 
    //  Toggle('repeat'); sets repeat to true and shuffle to false
    //  Toggle('shuffle'); sets shuffle to true and repeat to false
    public toggle(prop: string, forceTrue?: boolean): any {
        let propName: string = (prop === 'shuffle') ? 'isShuffling' : (prop === 'repeat') ? 'isRepeating' : '';
        let oppositePropName: string = (prop === 'shuffle') ? 'isRepeating' :( prop === 'repeat') ? 'isShuffling' : '';
        this[propName] = (forceTrue !== undefined) ? forceTrue : !this[propName];
        if (forceTrue === undefined) {
            this[oppositePropName] = false;
        }
        if (propName === 'shuffle') {
            this.shuffleIndex = 0;
        }
        return this;
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Flush Everyting
     * Reset all attributes to defaults
     * Clear everythihg
     */
    private flush(reset?: boolean): any {
        if (reset) {
            this.isReloading = true;
            this.clearWaiters();
        }
        if (this.options.video && this.isUsingHLSPolyfill && this.hls) {
            this.destoryHLS();
        }
        this.bufferProgress = this.lastBufferProgress = this.progress = this.position = this.remainingTime = this.duration = 0;
        this.error = this.metadata = this.playState = undefined;
        this.currentMediaItem = {};

        if (!this.isUsingHLSPolyfill) {
            this.setAttribute('src', this.emptyWaveFileBase64); //this.options.html5 ? this.audioElement : this.audioSourceElement
        }
        this.setPosition(0);
        return this;
    }

    private clearWaiters(): any {
        w.clearTimeout(this.errorTimeout);
        this.errorTimeout = undefined;
        if (this.windows || this.playstation) {
            w.clearInterval(this.interval); 
            this.interval = undefined;
        } 
        else {
            this.onlinecbreload = false;
        }
        return this;
    }
    /****************************************************************************************************************************/




    /****************************************************************************************************************************
     * Utility Methods Everyting
     */  
    private setOptions (options: Object): any {
        if (options) {
            let opt: string;
            for (opt in options) {
                this.options[opt] = options[opt];
                if (opt === 'volume') {
                    this.setVolume(options[opt]);
                }
            }
        }
        return this;
    }

    private createMedia (): any {
        if (this.options.html5) {
            this.audioElement = new Audio();
            this.updateDomElement();
        } 
        else {
            if (this.options.hls) {
                this.hasHlsScript = d.getElementById('gl-hls');
                if (!!this.hasHlsScript) {
                    this.createElement();
                } else {
                     console.log('appending script');
                    this.script = d.createElement(s);
                    this.script.src = 'https://anghamiwebcdn.akamaized.net/web/vendor/hls.min.js';
                    this.script.id = 'gl-hls';
                    this.script.async = false;
                    d.head.appendChild(this.script);
                    
                    this.script.onload = () => {
                        console.log('script hls.js latest has loaded');
                        this.createElement();
                        delete this.script
                    }; 

                }
                delete this.hasHlsScript;
            //if (this.device.browser) {
                
            } else {
                this.createElement();
            }
            // }
            // else {
            //     this.createElement();
            // }
        }
        return this;
    }


    private createNewHlsContext (): void {
        this.hls = new w.Hls({
              autoStartLoad: true,
              startPosition : -1,
              capLevelToPlayerSize: true, //false,
              debug: false,
              defaultAudioCodec: undefined,
              initialLiveManifestSize: 1,
              maxBufferLength: 30,
              maxMaxBufferLength: 600,
              maxBufferSize: 60*1000*1000,
              maxBufferHole: 0.5,
              lowBufferWatchdogPeriod: 0.5,
              highBufferWatchdogPeriod: 3,
              nudgeOffset: 0.1,
              nudgeMaxRetry : 5,
              maxFragLookUpTolerance: 0.2,
              liveSyncDurationCount: 3,
              liveMaxLatencyDurationCount: 10,
              enableWorker: true,
              enableSoftwareAES: true,
              manifestLoadingTimeOut: 15000,
              manifestLoadingMaxRetry: 4,
              manifestLoadingRetryDelay: 500,
              manifestLoadingMaxRetryTimeout : 64000,
              startLevel: undefined,
              levelLoadingTimeOut: 10000,
              levelLoadingMaxRetry: 4,
              levelLoadingRetryDelay: 500,
              levelLoadingMaxRetryTimeout: 64000,
              fragLoadingTimeOut: 20000,
              fragLoadingMaxRetry: 6,
              fragLoadingRetryDelay: 500,
              fragLoadingMaxRetryTimeout: 64000,
              startFragPrefetch: false,
              appendErrorMaxRetry: 3,
              enableWebVTT: true,
              enableCEA708Captions: true,
              stretchShortVideoTrack: true,//false,
              maxAudioFramesDrift : 1,
              forceKeyFrameOnDiscontinuity: true,
              abrEwmaFastLive: 5.0,
              abrEwmaSlowLive: 9.0,
              abrEwmaFastVoD: 4.0,
              abrEwmaSlowVoD: 15.0,
              abrEwmaDefaultEstimate: 500000,
              abrBandWidthFactor: 0.95,
              abrBandWidthUpFactor: 0.7,
              minAutoBitrate: 0
        });
        // attach hlsjs to videotag
        this.hls.attachMedia(this.audioElement);
        this.appendHLSEvents();
    }

    private createElement (): any {
        let element: any;
        try {
            if (this.options.video) {

                // Any DOM element where the newely created video element will be injected 
                let videoElementParent: HTMLElement = d.getElementById(v);
                this.audioElement = videoElementParent.appendChild(d.createElement(v));
                videoElementParent = null;

                //either chrome or fireflox
                if (this.device.browser && this.options.hls) {  //!element.canPlayType('application/vnd.apple.mpegurl')

                    this.isUsingHLSPolyfill = true;

                    // let script: HTMLScriptElement = d.createElement(s);
                    // script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
                    // //script.async = true;
                    // d.head.appendChild(script);
                    
                    //script.onload = () => {
                        //script = null;
                    //};
                }
                this.updateDomElement();
                
            } else {
                this.audioElement = d.body.appendChild(d.createElement(a));
                this.updateDomElement();
            }
        } 
        catch(e) {} 
    }    

    private updateDomElement (): void {
        //alert('finally');
        this.addEventListeners();

        this.setAttribute('preload', 'none');
        // this.audioElement.preload = 'none';

        if (this.options.video) {
            // if (this.device.browser !== 'safari') {
                this.audioElement.playsInline = true;
                this.audioElement.setAttribute('webkit-playsinline', 'true');
            // }
            
            this.audioElement.style.width = '100%';
            this.audioElement.style.height = '100%';// '100%';
            this.audioElement.style.display = 'block';
            this.audioElement.style.position = 'absolute';
            this.audioElement.style.top = '0';
            this.audioElement.style.left = '0';
            this.audioElement.style.right = '0';
            this.audioElement.style.bottom = '0';
            this.audioElement.style.backgroundColor = 'black';

           
            this.setSrc(this.emptyVideoFileBase64)
                .setAttribute('type', this.options.hls ? 'application/x-mpegURL' : 'video/mp4');
                //.setAttribute('webkit-playsinline', true)
                //.setAttribute('playsinline', true)
                //.setAttribute('src', this.currentSrc);
            
        } else {
            // console.log('regular init without callback');
            //kthis.audioElement.preload = 'none';
            this.setSrc(this.emptyWaveFileBase64)
                 .setAttribute('type', 'audio/mp4; codecs="mp4a.40.2"');
                 //.setAttribute('src', this.currentSrc);//this.options.html5 ? this.audioElement : this.audioSourceElement
        }
   
        if (this.options.controls) {
            this.audioElement.controls = true;
        }
        if (this.options.poster) {
            this.setAttribute('poster', this.options.poster);
        }
        if (typeof this.options.onInit === 'function') {
         //this.dispatchEvent('init');
         this.options.onInit();
        }
    }

    private addEventListeners (): any {
        if (this.audioElement) {
            let e: string;
            for (e in this.audioElementEvents) {
                this.audioElement.addEventListener(e, this.audioElementEvents[e], false);
            }
            if (this.options.video && this.isUsingHLSPolyfill) {
                // create separate error handlers for hlsjs and the video tag
                this.hlsjsErrorHandler = this.hlsPolyfillVideoTagErrorHandlerFactory();
                this.hlsVideoTagErrorHandler = this.hlsPolyfillVideoTagErrorHandlerFactory();
            }
            console.log('adding event listeners', this);
        }
        return this;
    }

    private isValid(): boolean {
        return this.forceDurationChangeForAudioAd ? true : (this.currentSrc.match(this.mimeRegx) !== null);// > 0;
    }

    private isBufferValid(): boolean {
        return (this.bufferProgress > 0) && (this.bufferProgress !== this.lastBufferProgress);
    }

    private hasBuffer(): boolean {
        return !this.playstation && this.bufferProgress !== 100;
    }

    private setSrc(src: string): any {
        try {
            this.currentSrc = src;
        } catch(e) {}
        return this;
    }

    private setAttribute(type: string, value?: string): any {
        if (typeof value !== 'undefined') {
            try {
                this.audioElement[type] = value; 
            } 
            catch(e) {}
        }
        return this;
    }

     private removeAttribute(type: string): any {
        try {
            this.audioElement.removeAttribute(type);
            // delete this.audioElement[type]; 
        } 
        catch(e) {}
        return this;
    }
    /****************************************************************************************************************************/




    /***************************************************************************************************************************
     * Events Dispatcher
     */
    public on(type: string, callback: Function): void {
    	if (!this.eventMap[type]) {
    		this.eventMap[type] = [];
    	}
        this.eventMap[type].push(callback);
    }

    public off(type: string, filler: any): void {
    	this.eventMap[type] = filler;
    }

    private dispatchEvent(type: any, args?: Object): any { 
    	let objValue: any = this.eventMap[type];
    	if (!objValue) {
    		return this;
    	}
        objValue.forEach(function(item: Function) {
        	(typeof item === 'function') && item(args);
        });
        return this;
    }
    /***************************************************************************************************************************/



    /***************************************************************************************************************************
     * On Destory
     */
    public destroy(): any {
        let e: string; 
        if(this.audioElement) {
            for (e in this.audioElementEvents) {
                this.audioElement.removeEventListener(e, this.audioElementEvents[e], false);
            }
        }
        for (e in this.eventMap) {
            delete this.eventMap[e];
        }
       
        if (!this.options.html5 && this.audioElement) {
            this.audioElement.parentNode.removeChild(this.audioElement); 
        }
        if (this.options.video && this.isUsingHLSPolyfill) {
            this.destoryHLS();
            this.options = {};
            this.isUsingHLSPolyfill = undefined;
            delete this.hlsjsErrorHandler;
            delete this.hlsVideoTagErrorHandler;
            delete this.hls;
        }
        delete this.eventMap;
        if (this.audioElement) {
            delete this.audioElement;
        }
        this.eventMap = {};
        this.audioElement = undefined;
        return this;
    }

    private destoryHLS (): void {
        if (this.hls) {
            this.hls.destroy();
            this.hls.detachMedia();
        }
    }
    /***************************************************************************************************************************/



    /****************************************************************************************************************************
     * Error Police Management
     */
    private hlsPolyfillVideoTagErrorHandlerFactory (): Function {
        let _recoverDecodingErrorDate: number = null;
        let _recoverAudioCodecErrorDate: number = null;

        return () => {
            const now: number = Date.now();

            if (!_recoverDecodingErrorDate || (now - _recoverDecodingErrorDate) > 2000) {
                _recoverDecodingErrorDate = now;
                this.hls.recoverMediaError();
            }
            else if (!_recoverAudioCodecErrorDate || (now - _recoverAudioCodecErrorDate) > 2000) {
                _recoverAudioCodecErrorDate = now;
                this.hls.swapAudioCodec();
                this.hls.recoverMediaError();
            }
            else {
                console.error('Error loading media: File could not be played ');
                this.destoryHLS();
            }
        };
    }
    /***************************************************************************************************************************/



    /****************************************************************************************************************************
     * Error Police Management
     */
    private checkError(e: Event): boolean {
        // this.audioTrackPlaying = undefined;
        if (this.isValid()) {
            this.catchErrorVulnerability(e);
            // this.dispatchEvent('wait', {status: this.audioTrackPlaying});
        }
        return;
    }

    private showLoading (): void {
        this.audioTrackPlaying = undefined;
        this.dispatchEvent('wait', {status: this.audioTrackPlaying});
    }

    private catchErrorVulnerability(e: any): any {
        if (this.bufferProgress === 100) {
            return;
        }

        let readyState: any = e.target.readyState;  
        let networkState: any = e.target.networkState;
        let safeState: boolean = readyState >= 2 && this.metadata;
        let isLoading: boolean = networkState === 2;
        let error: any = e.target.error;
        let code: number = error && error.code ? error.code : undefined;
        
        let wait: Function = (time?: number) => {
            this.showLoading();
            this.error = true;
            this.waitUntilOnline();
        };
        let callReload: Function = (time?: number) => {
            this.showLoading();
            this.error = true;
            this.reload(time);
        };

        this.online = this.windows || this.playstation ? this.isOnline() : this.online;
        
        /*  *** error ***
            *************
            1 = MEDIA_ERR_ABORTED               fetching process aborted by user
            2 = MEDIA_ERR_NETWORK               error occurred when downloading
            3 = MEDIA_ERR_DECODE                  error occurred when decoding
            4 = MEDIA_ERR_SRC_NOT_SUPPORTED      audio/video not supported
                
               *** networkState ***
            ********************
             0 = NETWORK_EMPTY              audio/video has not yet been initialized
            1 = NETWORK_IDLE              audio/video is active and has selected a resource, but is not using the network - no internet not loading
            2 = NETWORK_LOADING          browser is downloading data - is loading
            3 = NETWORK_NO_SOURCE          no audio/video source found - empty source

           *** readyState ***
            ******************
            0 = HAVE_NOTHING              no information whether or not the audio/video is ready
            1 = HAVE_METADATA           metadata for the audio/video is ready
            2 = HAVE_CURRENT_DATA       data for the current playback position is available, but not enough data to play next frame/millisecond
            3 = HAVE_FUTURE_DATA          data for the current and at least the next frame is available
            4 = HAVE_ENOUGH_DATA          enough data available to start playing
        */

        if (this.online) { /*** browser is online **/

            // cannot reload so dispatch a fatal error to fix the track src, url, etc or to move next
            if (error && code === 4) {  //|| (networkState === 3 && readyState === 0) 
                this.showLoading();
                this.dispatchEvent('error', { progress: this.progress });
                return;
            }
            
            // regular error reload on user's given time
            else if ( (error && code > 1) 
                      || 
                      ((this.bufferProgress > 0 && !isLoading && !safeState) 
                      || 
                      (this.playstation && this.playState))
                    ) 
                 { 
                callReload(this.options.reloadOnError); 
            } 

            else if (this.bufferProgress === 0 || (this.playstation && !this.playState)) {

                // Audio is loading but not safe so give it a maximum time limit of user's input plus 10 seconds in advance
                if (isLoading && !safeState) { 
                    callReload(this.options.reloadOnInit + 10000); 
                } 

                // Audio is not loading and not safe so reload on user's given time
                else if ((!isLoading && !safeState) || (safeState && !isLoading)) { 
                    callReload(this.options.reloadOnInit);
                } 
            } 
        }
        else { /*** browser is offline **/ 
            // wait until online and then reload
            wait(1000);
        }
    }
    /****************************************************************************************************************************/
}

// let galactic: Galactic = new Galactic();

if (typeof module === 'object' && module && typeof module.exports === 'object') {
  	/**
   	 * commonJS module
   	 */
  	module.exports = Galactic;
}
else if (typeof define === 'function' && define.amd) {

	/**
     * AMD - requireJS
     * basic usage:
     * require(["/path/to/Galactic.js"], function(Galactic) {
     *   Galactic.instance.init()
     * });
     *
    */
  	define(function() {
    	return {
      		constructor: Galactic,
      		instance: Galactic
    	};
  	});
}
else {
    w.Galactic = Galactic;
}