import Math;
import Game;

namespace Audio {

    enum _MusicState {
        NOT_PLAYING = 1,
        TRANSITION_IN = 2,
        PLAYING_NORMAL = 3,
        PLAYING_LOOPING = 4,
        TRANSITION_OUT = 5,
    }

    /*
        nativeData[0] -> native music object, if necessary. Some platforms just do everything by path.
    */
    @localized('en', 'Music')
    @localized('es', 'ESMusic')
    @localized('jp', 'JPMusic')
    class Music {

        static field _isSupported = Game.AudioImpl_isSupported();

        field _isResource;
        field _path;

        @private
        constructor() {
            // Initialized by factory methods.
        }

        static function _convertPath(path) {
            fileinfo = Game.AudioImpl_getAudioResourcePath(path + '');
            if (fileinfo == null) {
                throw new MusicNotFoundException("Audio resource '" + path + "' not found.");
            } else if (!fileinfo[0]) {
                throw new Core.InvalidArgumentException("Resource '" + path + "' is not an audio file.");
            }
            return fileinfo[1];
        }

        @localized('en', 'loadFromResource')
        @localized('es', 'ESloadFromResource')
        @localized('jp', 'JPloadFromResource')
        static function loadFromResource(path) {

            path = Music._convertPath(path);

            music = new Music();
            music._isResource = true;
            music._path = path;
            if (!Game.AudioImpl_musicLoadFromResource(music, path)) {
                throw new MusicNotFoundException("Music resource '" + path + "' not found.");
            }
            return music;
        }

        static field currentState = _MusicState.NOT_PLAYING;
        static field currentMusic = null;
        static field currentIsLoop = false;
        static field currentInTransitionDuration = null;
        static field currentOutTransitionDuration = null;
        static field currentTransitionStart = null;

        static field enqueuedMusic = null;
        static field enqueuedIsLoop = null;
        static field enqueuedInTransitionDuration = null;

        static field mainVolume = 1.0; // overall volume
        static field compositeVolume = 1.0; // volume with fades and song normalization applied
        static field fadeVolume = 1.0;

        static constructor() {
            Game.GameWindow._frameEndHandlers.add(Music._gameWindowFrameEndHandler);
        }

        static function _gameWindowFrameEndHandler() {
            switch (Music.currentState) {
                case _MusicState.NOT_PLAYING:
                    // nothing to do
                    break;

                case _MusicState.TRANSITION_IN:
                    dt = Core.currentTime() - Music.currentTransitionStart;
                    if (dt >= Music.currentInTransitionDuration) {
                        Music.fadeVolume = 1.0;
                        Music.currentState = Music.currentIsLoop ? _MusicState.PLAYING_LOOPING : _MusicState.PLAYING_NORMAL;
                    } else {
                        Music.fadeVolume = 1.0 * dt / Music.currentInTransitionDuration;
                    }
                    break;

                case _MusicState.PLAYING_NORMAL:
                    if (!Game.AudioImpl_musicIsPlaying()) {
                        // Rather than just mark this as no longer playing, pretend that it's a transition out that's already done.
                        // This will either mark it as NOT_PLAYING or will automatically pop the queue and start the next song.
                        // Otherwise enqueuing a song during a non-looping song and if the song just happens to end while it's still fading
                        // the next song won't get triggered and that will make users cry.
                        Music.currentState = _MusicState.TRANSITION_OUT;
                        Music.currentTransitionStart = 0;
                        Music.currentOutTransitionDuration = 1.0;
                    }
                    break;

                case _MusicState.PLAYING_LOOPING:
                    // carry on.
                    break;

                case _MusicState.TRANSITION_OUT:
                    dt = Core.currentTime() - Music.currentTransitionStart;
                    if (dt == null || dt >= Music.currentOutTransitionDuration) {
                        Music.fadeVolume = 0.0;
                        // music fade complete.
                        if (Music.enqueuedMusic == null) {
                            Music.currentState = _MusicState.NOT_PLAYING;
                            Music.currentMusic = null;
                            Game.AudioImpl_musicStop();
                        } else {
                            Music.currentMusic = Music.enqueuedMusic;
                            Music.currentIsLoop = Music.enqueuedIsLoop;
                            if (Music.enqueuedInTransitionDuration == null) {
                                found = Game.AudioImpl_musicPlay(
                                    Music.currentMusic,
                                    Music.mainVolume,
                                    Music.currentIsLoop);
                                if (!found) throw new Audio.MusicNotFoundException("Music file not found: " + Music.currentMusic._path);

                                Music.currentState = Music.currentIsLoop
                                    ? _MusicState.PLAYING_LOOPING
                                    : _MusicState.PLAYING_NORMAL;
                                Music.currentInTransitionDuration = null;
                                Music.fadeVolume = 1.0;
                            } else {
                                Game.AudioImpl_musicPlay(
                                    Music.currentMusic,
                                    0.0,
                                    Music.currentIsLoop);
                                Music.currentState = _MusicState.TRANSITION_IN;
                                Music.currentInTransitionDuration = Music.enqueuedInTransitionDuration;
                                Music.currentTransitionStart = Core.currentTime();
                            }
                            Music.enqueuedMusic = null;
                        }
                    } else {
                        Music.fadeVolume = 1.0 - dt / Music.currentOutTransitionDuration;
                    }
                    break;
            }

            v = Math.ensureRange(Music.fadeVolume * Music.mainVolume, 0.0, 1.0);
            diff = Math.abs(v - Music.compositeVolume);
            if (diff > 0.00001) {
                Music.compositeVolume = v;
                Game.AudioImpl_musicSetVolume(v);
            }
        }

        @localized('en', 'getCurrent')
        @localized('es', 'ESgetCurrent')
        @localized('jp', 'JPgetCurrent')
        static function getCurrent() {
            return Music.currentMusic;
        }

        @localized('en', 'fadeOutAndIn')
        @localized('es', 'ESfadeOutAndIn')
        @localized('jp', 'JPfadeOutAndIn')
        function fadeOutAndIn(loop = true, outTransition = 2.0, inTransition = 2.0) {
            Music._transition(this, loop, outTransition, inTransition);
        }

        @localized('en', 'fadeOutAndPlay')
        @localized('es', 'ESfadeOutAndPlay')
        @localized('jp', 'JPfadeOutAndPlay')
        function fadeOutAndPlay(loop = true, outTransition = 2.0) {
            Music._transition(this, loop, outTransition, null);
        }

        @localized('en', 'play')
        @localized('es', 'ESplay')
        @localized('jp', 'JPplay')
        function play(loop = true) {
            Music._transition(this, loop, null, null);
        }

        @localized('en', 'fadeOut')
        @localized('es', 'ESfadeOut')
        @localized('jp', 'JPfadeOut')
        function fadeOut(duration = 2.0) {
            if (Music.currentMusic == this) {
                Music.fadeOutCurrent(duration);
            }
        }

        @localized('en', 'fadeOutCurrent')
        @localized('es', 'ESfadeOutCurrent')
        @localized('jp', 'JPfadeOutCurrent')
        static function fadeOutCurrent(duration = 2.0) {
            Music._transition(null, false, duration, null);
        }

        @localized('en', 'stop')
        @localized('es', 'ESstop')
        @localized('jp', 'JPstop')
        static function stop() {
            Music._transition(null, false, null, null);
        }

        @localized('en', 'fadeIn')
        @localized('es', 'ESfadeIn')
        @localized('jp', 'JPfadeIn')
        function fadeIn(loop = true, duration = 2.0) {
            Music._transition(this, loop, null, duration);
        }

        /*
            music can be null to indicate a top
            outTransition can be null to indicate instant end
            inTransition can be null to indicate instant start
        */
        static function _transition(music, loop, outTransitionDuration, inTransitionDuration) {
            if (inTransitionDuration == null || inTransitionDuration < 0.01) inTransitionDuration = null;
            if (outTransitionDuration == null || outTransitionDuration < 0.01) outTransitionDuration = null;
            if (music == Music.currentMusic) return;
            loop = loop == true;

            if (outTransitionDuration == null) {
                // instantaneous transitions
                if (music == null) {
                    Game.AudioImpl_musicStop();
                    Music.currentMusic = null;
                    Music.currentState = _MusicState.NOT_PLAYING;
                } else {
                    Music.currentMusic = music;
                    Music.currentIsLoop = loop;
                    if (inTransitionDuration == null) {
                        Music.currentState = loop ? _MusicState.PLAYING_LOOPING : _MusicState.PLAYING_NORMAL;
                        Music.fadeVolume = 1.0;
                        Game.AudioImpl_musicPlay(music, Music.mainVolume, loop);
                    } else {
                        Music.currentState = _MusicState.TRANSITION_IN;
                        Music.fadeVolume = 0.0;
                        Music.currentTransitionStart = Core.currentTime();
                        Music.currentInTransitionDuration = inTransitionDuration;
                        Game.AudioImpl_musicPlay(music, 0.0, loop);
                    }
                }
            } else {
                // fade out. save transition information for the end-frame worker.
                Music.currentState = _MusicState.TRANSITION_OUT;
                Music.currentTransitionStart = Core.currentTime();
                Music.currentOutTransitionDuration = outTransitionDuration;
                Music.enqueuedMusic = music;
                Music.enqueuedInTransitionDuration = inTransitionDuration;
                Music.enqueuedIsLoop = loop;
            }
        }
    }
}
