//=============================================================================
// Aetherflow Plugins - Preload Everything
// Aetherflow_PreloadEverything.js
//=============================================================================
/*:
 * @plugindesc v1.0.2 Preload All The Things. (IF YOU HAVE v1.6.1)
 * @author Kaliya
 *
 * @param ReleaseMapChange
 * @text Release On Map Change
 * @type boolean
 * @default true
 * @desc Release non-system/global preloaded assets from memory on map change?
 * 
 * @param PreloadSystem
 * @text Preload System Resources
 * @type boolean
 * @default true
 * @desc Preload all system based assets on start?
 * 
 * @param ImageCacheSize
 * @text Image Cache Size
 * @desc Size of the image cache in bytes.
 * @type text
 * @default 10 * 1000 * 1000
 * 
 * @param AudioCacheSize
 * @text Audio Cache Size
 * @desc Size of the audio cache in bytes.
 * @type text
 * @default 20 * 1000 * 1000
 * 
 * @param PreloadGlobal
 * @text Global Assets
 * @type struct<PreloadContainer>
 * @desc Preloaded assets that are kept in memory on game start.
 * 
 * @param PreloadMaps
 * @text Map Assets
 * @desc A list of Preload Containers used for map assets.
 * @type struct<PreloadMapContainer>[]
 *  
 * @help
 * ---------------------------------------------------------------------------------
 * Introduction
 * ---------------------------------------------------------------------------------
 * This plugin is designed to allieviate your "preloading" problems. Tired of not
 * having your sound play right away when you need it? Tired of some images not 
 * appearing? Well look no further because those days are a thing of the past. Not 
 * only does this plugin preload your thingys, but it also acts as a basic caching 
 * system to help with the problem of memory being overused (if done right). It is
 * not a perfect solution, but it should help all games.
 * 
 * NOTE: THIS PLUGIN REQUIRES RPG MAKER MV V1.6.1 IF YOU ARE NOT USING V1.6.1
 * THIS PLUGIN WILL NOT WORK AND WILL ERROR OUT. YOU HAVE BEEN WARNED.
 * 
 * ---------------------------------------------------------------------------------
 * How-to
 * ---------------------------------------------------------------------------------
 * The system is fairly straightforward, it uses "containers" to preload and store
 * resources. These containers come in two flavors:
 * 
 * GLOBAL: This is a singular container, it is used to store Global, or game wide assets
 * these assets are usually things you always want to be loaded. They are all loaded on
 * game start.
 * 
 * MAP: These containers are what you use to preload map resources. These containers
 * are loaded when the map associated with them is entered. Depending on your plugin
 * parameters they will also be released from the cache when you enter a new map.
 * 
 * In order to use the containers you simply modify and add to them via plugin parameters.
 * 
 * ---------------------------------------------------------------------------------
 * Caching & How It Works
 * ---------------------------------------------------------------------------------
 * 
 * The caching system stores assets in memory until they are no longer needed and
 * are manually purged via the available functions, or by being deleted by the system
 * itself when a new resource is loaded and the cache is full.
 * 
 * The cache size is determined by two plugin parameters:
 * 
 * Image Cache Size: The size of the image cache in bytes.
 * 
 * Audio Cache Size: The size of the audio cache in bytes.
 * 
 * These two plugin parameters are evaluated and thus you can use math to determine
 * what the size rather than having to write out a large number. (e.g: 10 * 1000 * 1000 is a 10MB cache).
 * 
 * You can use various functions in the cache classes to "Reserve" a resource. These
 * "reserved" resources will not be automatically purged, so be careful!
 * 
 * When a resource is added to the cache, the cache is sorted by the time the resources were all
 * last touched and then if a resource is not reserved it is purged and will need to be reacquired
 * again when next used.
 * 
 * ---------------------------------------------------------------------------------
 * Containers
 * ---------------------------------------------------------------------------------
 * For those users who are more hands on, the caching system has a type of object
 * known as a "Container". Think of this as a group of objects, that can all be cached
 * together and then can all be purged together at the same time, for easy cleanup and
 * management. These are not the same as containers you setup in the plugin parameters.
 * These containers are only accessed and used via code.
 * 
 * Various function exists to help with the ease and usage of containers. They are
 * all well documented throughout the plugin but a brief overview of them is as follows:
 * 
 * ImageManager/AudioManager.addContainer("containerName"): Adds a container to the
 * specified resource cache for that type.
 * 
 * ImageManager/AudioManager.addContainerItem("containerName", "itemKey"): Adds an item
 * to the container. NOTE: It only adds a key to look up the item in the cache for purging,
 * it does not preload or load an item for you, you will need to do that prior to adding it
 * to a container.
 * 
 * ImageManager/AudioManager.releaseContainer("containerName"): Releases all objects
 * within the specified container from the cache.
 * 
 * ImageManager/AudioManager.releaseContainerItem("containerName", "itemKey"): Releases
 * the specified item from the specified container if it exists, freeing it from the
 * cache.
 * 
 * ---------------------------------------------------------------------------------
 * Credits
 * ---------------------------------------------------------------------------------
 * This plugin is developed and maintained by Liquidize/Kaliya.
 * Some code was ninja'ed with permission from Pivoo (http://pivoo.me)
 * 
 * ---------------------------------------------------------------------------------
 * Changelog
 * ---------------------------------------------------------------------------------
 * 
 * 1.0.2:
 * - Fixed a bug wherein due to slight changes in handling paths, Map based container for images were not being loaded.
 * 
 * 1.0.1:
 * - Fixed a bug wherein due to a slight change before release, the path for audio files
 * would be incorrect.
 * 
 * 1.0.0:
 * - Released!
 * 
 */
/*~struct~PreloadContainer:
 @param Images
 @type file[]
 @dir images/
 @desc A list of images you want to preload for this container.

 @param Audio
 @type file[]
 @dir audio/
 @desc A list of audio files you want to preload for this container.
*/
/*~struct~PreloadMapContainer:
 @param MapId
 @text Map ID
 @type number
 @desc The id the of map for this container.
 @min 0
 @max 999
 @default 0

 @param Images
 @type file[]
 @dir images/
 @desc A list of images you want to preload for this container.

 @param Audio
 @type file[]
 @dir audio/
 @desc A list of audio files you want to preload for this container.
*/

//=============================================================================
// PluginManager
//
// NOTE: The parameter parsing functions were originally written by Pivoo (http://pivoo.me)
// he has kindly given me permission to include and modify them but all credit should 
// go to him.
//=============================================================================

PluginManager.parseObject = function (obj) {
    var output = new Object();
    Object.keys(obj).forEach(function (key) {
        var param = obj[key];
        output[key] = this.parse(param, key);
    }, this);
    return output;
};

PluginManager.parseArray = function (arr, key) {
    return arr.map(function (param, index) {
        return this.parse(param, key + '[' + index + ']');
    }, this);
};

PluginManager.parse = function (param, key) {
    if (!isNaN(param) && param.contains('.')) {
        return parseFloat(param);
    } else {
        try {
            param = JSON.parse(param);
            switch (param.constructor.name) {
                case 'Object': return this.parseObject(param);
                case 'Array': return this.parseArray(param, key);
                default: return param;
            }
        } catch (err) {
            return param;
        }
    }
};

//=============================================================================
// Configuration
//=============================================================================

var Aetherflow = Aetherflow || {};
Aetherflow.Preload = Aetherflow.Preload || {};

Aetherflow.Parameters = PluginManager.parameters('Aetherflow_PreloadEverything');
Aetherflow.Param = Aetherflow.Param || {};

Aetherflow.Param["ReleaseMapChange"] = Boolean(Aetherflow.Parameters["ReleaseMapChange"]);
Aetherflow.Param["PreloadSystem"] = eval(Aetherflow.Parameters["PreloadSystem"]);
Aetherflow.Param["PreloadConnections"] = Boolean(Aetherflow.Parameters["PreloadConnections"]);
Aetherflow.Param["GlobalContainer"] = Aetherflow.Parameters["PreloadGlobal"].length > 0 ? JSON.parse(Aetherflow.Parameters["PreloadGlobal"]) : {};
Aetherflow.Param["MapContainers"] = Aetherflow.Parameters["PreloadMaps"].length > 0 ? JSON.parse(Aetherflow.Parameters["PreloadMaps"]) : {};
Aetherflow.Param["ImageCacheSize"] = Number(eval(Aetherflow.Parameters["ImageCacheSize"])) || 10 * 1000 * 1000;
Aetherflow.Param["AudioCacheSize"] = Number(eval(Aetherflow.Parameters["AudioCacheSize"])) || 20 * 1000 * 1000;

// Parse the actual data into the Preload Object for referencing easier.
Aetherflow.Preload.GlobalContainer = PluginManager.parseObject(Aetherflow.Param.GlobalContainer);
Aetherflow.Preload.MapContainers = Object.values(PluginManager.parseObject(Aetherflow.Param.MapContainers));

//=============================================================================
// End Configuration
//=============================================================================

//=============================================================================
// Game_System
//=============================================================================

Game_System.prototype.releaseGlobalContainer = function () {
    ImageManager.releaseGlobalContainer();
    AudioManager.releaseGlobalContainer();
};

//=============================================================================
// WebAudio
//=============================================================================

WebAudio.prototype.bufferSize = function () {
    return this._buffer ? this._buffer.length : null;
};

//=============================================================================
// ImageCache
//=============================================================================

Aetherflow.Preload.ImageCache_initialize = ImageCache.prototype.initialize;
ImageCache.prototype.initialize = function () {
    this._containers = {};
    Aetherflow.Preload.ImageCache_initialize.call(this);
};

ImageCache.prototype.releaseItem = function (key) {
    if (this._items[key]) {
        delete this._items[key];
        return true;
    }
    return false;
};

ImageCache.prototype.addContainer = function (container) {
    this._containers = this._containers || {};
    if (!this._containers[container]) {
        this._containers[container] = [];
    }
};

ImageCache.prototype.addContainerItem = function (container, itemKey) {
    if (this._containers[container]) {
        this._containers[container].push(itemKey);
    }
};

ImageCache.prototype.releaseContainer = function (container) {
    if (!this._containers) this._containers = {};
    if (!this._containers[container]) return false;
    for (let key of this._containers[container]) {
        this.releaseItem(key);
    }
    delete this._containers[container];
    return true;
};

ImageCache.prototype.releaseContainerItem = function (container, itemKey) {
    if (!this._container) this._containers = {};
    if (!this._containers[container]) return false;
    let index = this._containers[container].indexOf(itemKey);
    if (index >= 0) {
        this._containers[container].splice(index, 1);
        return this.releaseItem(itemKey);
    }
    return false;
};

//=============================================================================
// ImageManager
//=============================================================================

ImageManager.addContainer = function (container) {
    return this._imageCache.addContainer(container);
};

ImageManager.addContainerItem = function (container, key) {
    return this._imageCache.addContainerItem(container, key);
};

ImageManager.releaseItem = function (img, hue) {
    let key = this._generateCacheKey(img, hue);
    return this._imageCache.releaseItem(key);
};

ImageManager.releaseContainer = function (container) {
    return this._imageCache.releaseContainer(container);
};

ImageManager.releaseContainerItem = function (container, img, hue) {
    let key = this._generateCacheKey(img, hue);
    return this._imageCache.releaseContainerItem(container, key);
};

ImageManager.releaseGlobalContainer = function () {
    return this.releaseContainer("global");
};

//=============================================================================
// AudioCache
//=============================================================================

function AudioCache() {
    this.initialize.apply(this, arguments);
}

AudioCache.maxSize = Aetherflow.Param["AudioCacheSize"];

AudioCache.prototype.initialize = function () {
    this._items = {};
    this._containers = {};
};

AudioCache.prototype.add = function (key, value) {
    this._items[key] = {
        buffer: value,
        touch: Date.now(),
        key: key
    };
    this.truncateCache();
};

AudioCache.prototype.get = function (key) {
    if (this._items[key]) {
        let item = this._items[key];
        item.touch = Date.now();
        return item.buffer;
    }
    return null;
};

AudioCache.prototype.releaseItem = function (key) {
    if (this._items[key]) {
        delete this._items[key];
        return true;
    }
    return false;
};

AudioCache.prototype.addContainer = function (container) {
    this._containers = this._containers || {};
    if (!this._containers[container]) {
        this._containers[container] = [];
    }
};

AudioCache.prototype.addContainerItem = function (container, itemKey) {
    if (this._containers[container]) {
        this._containers[container].push(itemKey);
    }
};

AudioCache.prototype.releaseContainer = function (container) {
    if (!this._containers) this._containers = {};
    if (!this._container[container]) return false;
    for (let key of this._containers[container]) {
        this.releaseItem(key);
    }
    delete this._containers[container];
    return true;
};

AudioCache.prototype.releaseContainerItem = function (container, itemKey) {
    if (!this._containers) this._containers = {};
    if (!this._containers[container]) return false;
    let index = this._containers[container].indexOf(itemKey);
    if (index >= 0) {
        this._containers[container].splice(index, 1);
        return this.releaseItem(itemKey);
    }
    return false;
};

AudioCache.prototype.reserve = function (key, value, reservationId) {
    if (!this._items[key]) {
        this._items[key] = {
            buffer: value,
            touch: Date.now(),
            key: key
        };
    }
    this._items[key].reservationId = reservationId;
};

AudioCache.prototype.releaseReservation = function (reservationId) {
    let items = this._items;
    let release = false;
    Object.keys(items).map(function (key) {
        return items[key];
    }).forEach(function (item) {
        if (item.reservationId === reservationId)
            release = true;
        delete item.reservationId;
    });
    return release;
};

AudioCache.prototype.truncateCache = function () {
    let items = this._items;
    let sizeLeft = AudioCache.maxSize;
    Object.keys(items).map(function (key) {
        return items[key];
    }).sort(function (a, b) {
        return b.touch - a.touch;
    }).forEach(function (item) {
        if (sizeLeft > 0 || this.mustBeHeld(item)) {
            sizeLeft -= item.buffer.bufferSize() ? item.buffer.bufferSize() : 0; // If buffer isn't available than it isn't loaded.
        } else {
            delete items[item.key];
        }
    }.bind(this));
};

AudioCache.prototype.mustBeHeld = function (item) {
    if (item.reservationId) return true;
    if (!item.buffer.isReady()) return true;
    return false;
};

AudioCache.prototype.isReady = function () {
    let items = this._items;
    return !Object.keys(items).some(function (key) {
        return !items[key].buffer.isReady();
    });
};

//=============================================================================
// AudioManager
//=============================================================================

AudioManager._cache = new AudioCache();
AudioManager._systemReservationId = Utils.generateRuntimeId();
AudioManager._defaultReservationId = "default";
window._aufs = require("fs");

AudioManager._generateCacheKey = function (path) {
    return path;
};

AudioManager.clear = function () {
    this._cache = new AudioCache();
};

AudioManager.isReady = function () {
    return this._cache.isReady();
};

AudioManager.setDefaultReservationid = function (reservationId) {
    this._defaultReservationId = reservationId;
};

AudioManager.addContainer = function (container) {
    return this._cache.addContainer(container);
};

AudioManager.addContainerItem = function (container, itemKey) {
    return this._cache.addContainerItem(container, itemKey);
};

AudioManager.loadBgm = function (filename) {
    let path = this._path + "bgm/" + encodeURIComponent(filename) + this.audioFileExt();
    return this.loadAudio(path);
};

AudioManager.loadBgs = function (filename) {
    let path = this._path + "bgs/" + encodeURIComponent(filename) + this.audioFileExt();
    return this.loadAudio(path);
};

AudioManager.loadSe = function (filename) {
    let path = this._path + "se/" + encodeURIComponent(filename) + this.audioFileExt();
    return this.loadAudio(path);
};

AudioManager.loadMe = function (filename) {
    let path = this._path + "me/" + encodeURIComponent(filename) + this.audioFileExt();
    return this.loadAudio(path);
};

AudioManager.loadAudio = function (path) {
    path = _aufs.cachedAlternativeName(path);
    let key = this._generateCacheKey(path);
    let buffer = this._cache.get(key);
    if (!buffer) {
        buffer = new WebAudio(path);
        this._cache.add(key, buffer);
    }
    return buffer;
};

AudioManager.reserveBgm = function (filename, reservationId) {
    return this.reserveAudio("bgm/", filename, reservationId);
};

AudioManager.reserveBgs = function (filename, reservationId) {
    return this.reserveAudio("bgs/", filename, reservationId);
};

AudioManager.reserveSe = function (filename, reservationId) {
    return this.reserveAudio("se/", filename, reservationId);
};

AudioManager.reserveMe = function (filename, reservationId) {
    return this.reserveAudio("me/", filename, reservationId);
};

AudioManager.reserveAudio = function (folder, filename, reservationId) {
    if (filename) {
        let path = _aufs.cachedAlternativeName(this._path + folder + encodeURIComponent(filename) + this.audioFileExt());
        let buffer = new WebAudio(path);
        this._cache.reserve(this._generateCacheKey(path), buffer, reservationId || this._defaultReservationId);
        return buffer;
    } else {
        return null;
    }
};

AudioManager.releaseItem = function (key) {
    return this._cache.releaseItem(key);
};

AudioManager.releaseContainer = function (container) {
    return this._cache.releaseContainer(container);
};

AudioManager.releaseContainerItem = function (container, key) {
    return this._cache.releaseContainerItem(container, key);
};

AudioManager.releaseReservation = function (reservationId) {
    return this._cache.releaseReservation(reservationId);
};

AudioManager.releaseGlobalContainer = function () {
    return this.releaseContainer("global");
};

AudioManager.playBgm = function (bgm, pos) {
    if (this.isCurrentBgm(bgm)) {
        this.updateBgmParameters(bgm);
    } else {
        this.stopBgm();
        if (bgm.name) {
            if (Decrypter.hasEncryptedAudio && this.shouldUseHtml5Audio()) {
                this.playEncryptedBgm(bgm, pos);
            }
            else {
                this._bgmBuffer = this.loadBgm(bgm.name);
                this.updateBgmParameters(bgm);
                if (!this._meBuffer) {
                    this._bgmBuffer.play(true, pos || 0);
                }
            }
        }
    }
    this.updateCurrentBgm(bgm, pos);
};

AudioManager.playBgs = function (bgs, pos) {
    if (this.isCurrentBgs(bgs)) {
        this.updateBgsParameters(bgs);
    } else {
        this.stopBgs();
        if (bgs.name) {
            this._bgsBuffer = this.loadBgs(bgs.name);
            this.updateBgsParameters(bgs);
            this._bgsBuffer.play(true, pos || 0);
        }
    }
    this.updateCurrentBgs(bgs, pos);
};

AudioManager.playSe = function (se) {
    if (se.name) {
        this._seBuffers = this._seBuffers.filter(function (audio) {
            return audio.isPlaying();
        });
        let buffer = this.loadSe(se.name);
        this.updateSeParameters(buffer, se);
        buffer.play(false);
        this._seBuffers.push(buffer);
    }
};

AudioManager.playMe = function (me) {
    this.stopMe();
    if (me.name) {
        if (this._bgmBuffer && this._currentBgm) {
            this._currentBgm.pos = this._bgmBuffer.seek();
            this._bgmBuffer.stop();
        }
        this._meBuffer = this.loadMe(me.name);
        this.updateMeParameters(me);
        this._meBuffer.play(false);
        this._meBuffer.addStopListener(this.stopMe.bind(this));
    }
};

//=============================================================================
// DataManager
//=============================================================================

Aetherflow.Preload.DataManager_onLoad = DataManager.onLoad;
DataManager.onLoad = function (object) {
    Aetherflow.Preload.DataManager_onLoad.call(this, object);
    if (object === $dataSystem) {
        Scene_Boot.loadSystemAudio();
        Scene_Boot.loadGlobalContainer();
    }
};

//=============================================================================
// Scene_Base
//=============================================================================

Aetherflow.Preload.Scene_Base_isReady = Scene_Base.prototype.isReady;
Scene_Base.prototype.isReady = function () {
    return Aetherflow.Preload.Scene_Base_isReady.call(this) && AudioManager.isReady();
};

//=============================================================================
// Scene_Boot
//=============================================================================

Scene_Boot.loadSystemAudio = function () {
    if (Aetherflow.Param.PreloadSystem === true) {
        AudioManager.reserveBgm($dataSystem.airship.bgm.name, AudioManager._systemReservationId);
        AudioManager.reserveBgm($dataSystem.battleBgm.name, AudioManager._systemReservationId);
        AudioManager.reserveBgm($dataSystem.boat.bgm.name, AudioManager._systemReservationId);
        AudioManager.reserveBgm($dataSystem.ship.bgm.name, AudioManager._systemReservationId);
        AudioManager.reserveBgm($dataSystem.titleBgm.name, AudioManager._systemReservationId);
        AudioManager.reserveMe($dataSystem.gameoverMe.name, AudioManager._systemReservationId);
        AudioManager.reserveMe($dataSystem.defeatMe.name, AudioManager._systemReservationId);
        AudioManager.reserveMe($dataSystem.victoryMe.name, AudioManager._systemReservationId);

        $dataSystem.sounds.forEach(function (sound) {
            AudioManager.reserveSe(sound.name, AudioManager._systemReservationId);
        });
    }
};

Scene_Boot.loadGlobalContainer = function () {
    if (Aetherflow.Preload.GlobalContainer) {
        ImageManager.addContainer("global");
        AudioManager.addContainer("global");
        let images = Aetherflow.Preload.GlobalContainer.Images;
        if (images) {
            for (let image of images) {
                let folder = image.split('/')[0] + "/";
                let file = image.split('/')[1];
                let path = "img/" + folder;
                ImageManager.reserveBitmap(path, file, 0, true);
                ImageManager.addContainerItem("global", ImageManager._generateCacheKey(path, 0));
            }
        }
        let sounds = Aetherflow.Preload.GlobalContainer.Audio;
        if (sounds) {
            for (let sound of sounds) {
                let folder = sound.split('/')[0] + "/";
                let file = sound.split('/')[1];
                let path = require("fs").cachedAlternativeName("audio/" + folder + encodeURIComponent(file) + AudioManager.audioFileExt());
                AudioManager.reserveAudio(folder, file);
                AudioManager.addContainerItem("global", AudioManager._generateCacheKey(path));
            }
        }
    }
};



//=============================================================================
// Scene_Map
//=============================================================================

Scene_Map.prototype.releaseAssets = function () {
    let container = Aetherflow.Preload.MapContainers.find(function (element) {
        return element.MapId === $gameMap.mapId();
    });
    if (container) {
        if (container.Images && container.Images.length > 0) {
            let images = container.Images;
            for (let image of images) {
                let folder = image.split('/')[0];
                let file = image.split('/')[1];
                let path = "img/" + folder + "/" + encodeURIComponent(file) + ".png";
                ImageManager.releaseItem(path, 0);
            }
        }
        if (container.Audio && container.Audio.length > 0) {
            let sounds = container.Audio;
            for (let sound of sounds) {
                let folder = sound.split('/')[0];
                let file = sound.split('/')[1];
                AudioManager.releaseItem(require("fs").cachedAlternativeName("audio/" + folder + "/" + encodeURIComponent(file) + AudioManager.audioFileExt()));
            }
        }
    }
};

Scene_Map.prototype.preloadAssets = function () {
    let container = Aetherflow.Preload.MapContainers.find(function (element) {
        return element.MapId === $gameMap.mapId();
    });
    if (container) {
        let images = container.Images;
        if (images) {
            for (let image of images) {
                let folder = image.split('/')[0] + "/";
                let file = image.split('/')[1];
                let path = "img/" + folder + encodeURIComponent(file) + ".png";
                ImageManager.reserveNormalBitmap(path, 0, ImageManager._defaultReservationId);
            }
        }
        let sounds = container.Audio;
        if (sounds) {
            for (let sound of sounds) {
                let folder = sound.split('/')[0] + "/";
                let file = sound.split('/')[1];
                AudioManager.reserveAudio(folder, file, AudioManager._defaultReservationId);
            }
        }
    }
};

Aetherflow.Preload.Scene_Map_onMapLoaded = Scene_Map.prototype.onMapLoaded;
Scene_Map.prototype.onMapLoaded = function () {
    this.releaseAssets();
    Aetherflow.Preload.Scene_Map_onMapLoaded.call(this);
    this.preloadAssets();
};
