/*:
 * @target MZ
 * @author Aerosys
 * @plugindesc [Tier 1] [Version 1.3.0] [MV & MZ]
 * 
 * @help
 * 
 * ----------------------------------------------------------------------------
 * Rules
 * ----------------------------------------------------------------------------
 * 
 * 1. This Plugin is free of charge and can be used in any kind of game.
 * 
 * 2. You may not redistribute this Plugin or claim it as your own.
 *    
 *    a) Exception: You may redistribute this plugin as part of your game when
 *       releasing it.
 *    b) Exception: You may send this plugin to another person when you hire
 *       them for personal modifications.
 * 
 * 3. You may modify this plugin's source code for your needs but cannot
 *    redistribute your modifications.
 * 
 * 4. You may create plugins based on this (e.g. addon or extension) for your
 *    needs but you cannot redistribute them.
 * 
 * 
 * NEED SUPPORT?
 * Contact me: mail<at>aerosys.blog
 * 
 * 
 * ----------------------------------------------------------------------------
 * Quickstart
 * ----------------------------------------------------------------------------
 * 
 * This Plugin clones pre-defined Events from "Source Maps".
 * 
 * Any Map can be used as a Source Map and you can have multiple in your game.
 * To make a Map a Source Map, prepend a "$" or a "!" symbol to its name.
 * Use the $-symbol to make this Source Map always available through your game.
 * When using the !-symbol, the plugin will load this Source Map only when it's
 * a child map of the map where the player is right now.
 * 
 * Examples:
 * ! local Events
 * $ Events
 * 
 * Avoid using duplicate names!
 * 
 * 
 * ----------------------------------------------------------------------------
 * JavaScript
 * ----------------------------------------------------------------------------
 * 
 * MK.SpawnEvents.spawn(mapName, eventId, x, y)
 * - Spawns the Event from the Spawn Map to x, y
 * 
 * MK.SpawnEvents.spawnFromXy(mapName, xSource, ySource, xTarget, yTarget)
 * - Spawns the Event from that spot if there's an Event
 * - Otherwise, does nothing
 * 
 * MK.SpawnEvents.unspawn(eventId)
 * - Removes the Event with this Event ID, if it is a spawned one.
 * - Does nothing if the given Event is a regular one or does not exist at all.
 * 
 * MK.SpawnEvents.unspawnAt(x, y)
 * - Removes any spawned Event at this spot.
 * - Does not remove regular Events.
 * 
 * @endofhelp
 * 
 * @command spawnEvent
 * @text Spawn Event
 * 
 * @arg sourceMapName
 * @text Source Map Name
 * @desc Appending the ! or $ is not required.
 * @default REQUIRED
 * 
 * @arg eventId
 * @text Event Id
 * @type number
 * @default 1
 * 
 * @arg mode
 * @text Spawn Location: Mode
 * @type select
 * @option fixed
 * @option by Variables
 * @option Player's Position
 * @default fixed
 * 
 * @arg whenFixed
 * @parent mode
 * @text when fixed:
 * 
 * @arg x
 * @parent whenFixed
 * @text X Position
 * @type number
 * @default 1
 * 
 * @arg y
 * @parent whenFixed
 * @text Y Position
 * @type number
 * @default 1
 * 
 * @arg whenByVariables
 * @parent mode
 * @text when by Variables:
 * 
 * @arg variableX
 * @parent whenByVariables
 * @text X Position
 * @type variable
 * @default 1
 * 
 * @arg variableY
 * @parent whenByVariables
 * @text Y Position
 * @type variable
 * @default 1
 * 
 * 
 * @command spawnEventAtRegion
 * @text Spawn Event at Region
 * 
 * @arg sourceMapName
 * @text Source Map Name
 * @desc Appending the ! or $ is not required.
 * @default REQUIRED
 * 
 * @arg eventId
 * @text Event Id
 * @type number
 * @default 1
 * 
 * @arg regionId
 * @text Region ID
 * @type number
 * @default 1
 * 
 * @arg times
 * @text times
 * @type number
 * @default 1
 * 
 * 
 * @command unspawnAll
 * @text Unspawn All
 * 
 * 
 * @command unspawnBySourceEventId
 * @text Unspawn by Source Event Id
 * 
 * @arg sourceMapName
 * @text Source Map Name
 * @desc Appending the ! or $ is not required.
 * @default REQUIRED
 * 
 * @arg eventId
 * @type number
 * @default 1
 * 
 * 
 * @param resetEventEval
 * @text JS: Reset Event
 * @type note
 * @default "const mapId = arguments[0];\nconst eventId = arguments[1];\n\n$gameSelfSwitches.setValue([mapId, eventId, 'A'], false);\n$gameSelfSwitches.setValue([mapId, eventId, 'B'], false);\n$gameSelfSwitches.setValue([mapId, eventId, 'C'], false);\n$gameSelfSwitches.setValue([mapId, eventId, 'D'], false);"
 * 
 */

var MK = MK || { };
MK.SpawnEvents = { };

(function() {


const PLUGIN_NAME = 'MK_SpawnEvents';

const reject = (reason) => {
    const message = (
        'An Error has occurred in the Plugin %1: %2 ' +
        'If the problem persists, contact the Plugin Creator.'
    ).format(PLUGIN_NAME, reason);
    throw Error(message);
}

if (!PluginManager._parameters[PLUGIN_NAME.toLowerCase()]) {
    reject((
        'Please check that this plugin\'s filename is "%1.js". ' +
        'Subdirectories (e.g.: js/plugins/xy/thisPlugin.js) are not allowed.'
    ).format(PLUGIN_NAME));
}

const customFunction = (body, parameterName) => {
    if (!body) {
        reject((
            'The Plugin Parameter "%1" is missing. ' +
            'Please check it in the Plugin Manager. It may help to re-install this Plugin (i.e.: remove, re-add).'
        ).format(parameterName));
    }
    try {
        return new Function(JSON.parse(body));
    
    } catch (e) {
        reject((
            'The Plugin Parameter "%1" contains an error and could not be interpreted. ' +
            'Please check it in the Plugin Manager. It may also help to re-install this Plugin (i.e.: remove, re-add). ' +
            'Cause: %2'
        ).format(parameterName, e));
    }
}

const params = PluginManager.parameters(PLUGIN_NAME);
const resetEventEval = customFunction(params.resetEventEval);

const _files    = { };
const eventData = { };

function shouldLoadThisMap (info, mapId) {
    if (info) {
        return info.name.startsWith('$') || (info.name.startsWith('!') && info.parentId == mapId);
    }
}

const _SceneMap_create = Scene_Map.prototype.create;
Scene_Map.prototype.create = function() {
    _SceneMap_create.call(this);

    const mapId = $gamePlayer.isTransferring()
        ? $gamePlayer.newMapId()
        : $gameMap.mapId();
    
    MK.SpawnEvents._nMapsToLoad = $dataMapInfos
        .filter(info => shouldLoadThisMap(info, mapId))
        .length;

    $dataMapInfos
        .filter(info => shouldLoadThisMap(info, mapId))
        .forEach(info => loadMap(info.id, info.name));
}

const _SceneMap_isReady = Scene_Map.prototype.isReady;
Scene_Map.prototype.isReady = function() {
    return (
        _SceneMap_isReady.call(this) &&
        !MK.SpawnEvents._nMapsToLoad
    );
}

function loadMap(mapId, mapName) {
    const trimmedMapName    = mapName.replace('$', '').replace('!', '').trim().toLowerCase();
    const source            = 'Map%1'.format(mapId.padZero(3));
    const url               = 'data/Map%1.json'.format(mapId.padZero(3));

    loadFile(mapName, source, url, data => {
        data.events
            .filter(Boolean)
            .forEach(event => DataManager.extractMetadata(event));
        eventData[trimmedMapName] = data.events;
        MK.SpawnEvents._nMapsToLoad--;
    });
}

function loadFile(name, source, url, onLoad) {
    _files[url]
        ? onLoad(_files[url])
        : loadFileXhr(name, source, url, data => onLoad(_files[url] = data));
}

function loadFileXhr(name, source, url, onLoad) {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', url);
    xhr.overrideMimeType('application/json');
    xhr.onload = () => onLoad(JSON.parse(xhr.responseText));
    xhr.onerror = () => DataManager.onXhrError(name, source, url);
    xhr.send();
}


const _GameEvent_event = Game_Event.prototype.event;
Game_Event.prototype.event = function() {
    return _GameEvent_event.call(this) || getEventData(this.eventId());
}

const _GameMap_events = Game_Map.prototype.events;
Game_Map.prototype.events = function() {
    return _GameMap_events.call(this).concat(this.spawnedEventsMK());
}

const _GameMap_event = Game_Map.prototype.event;
Game_Map.prototype.event = function(eventId) {
    const event = _GameMap_event.call(this, eventId);

    if (event) {
        return event;
    }
    if (this._mk_spawnedEvents && this._mk_spawnedEvents[eventId]) {
        return this._mk_spawnedEvents[eventId];
    }
}

Game_Map.prototype.spawnedEventsMK = function() {
    return this._mk_spawnedEvents
        ? this._mk_spawnedEvents.filter(Boolean)
        : [ ];
}

Game_Map.prototype.pushSpawnedEvent = function(newEventId, event) {
    this._mk_spawnedEvents = this._mk_spawnedEvents || [ ];
    this._mk_spawnedEvents[newEventId] = event;
}

function removeFromLocalSpawnedEvents(eventId) {
    $gameMap._mk_spawnedEvents = $gameMap._mk_spawnedEvents || [ ];
    $gameMap._mk_spawnedEvents[eventId] = undefined;
}

const _GameMap_setupEvents = Game_Map.prototype.setupEvents;
Game_Map.prototype.setupEvents = function() {
    _GameMap_setupEvents.call(this);

    this._mk_spawnedEvents = [ ];

    this.persistedSpawnedEvents()
        .filter(event => fetchEventData(event.sourceMapName, event.sourceEventId))
        .forEach(event => {
            const data = fetchEventData(event.sourceMapName, event.sourceEventId);

            pushEventData(event.eventId, data, event.x, event.y);
            this.pushSpawnedEvent(event.eventId, new Game_Event(event.mapId, event.eventId));
        });
}

Game_Map.prototype.persistedSpawnedEvents = function() {
    return this._mk_persistedSpawnedEvents
        ? this._mk_persistedSpawnedEvents.filter(event => event && event.mapId == this.mapId())
        : [ ];
}

function getEventData(eventId) {
    return $gameMap._mk_eventData ? $gameMap._mk_eventData[eventId] : undefined;
}

function pushEventData(eventId, data, x, y) {
    $gameMap._mk_eventData = $gameMap._mk_eventData || [ ];

    $gameMap._mk_eventData[eventId] = JsonEx.makeDeepCopy(data);
    $gameMap._mk_eventData[eventId].id = eventId;
    $gameMap._mk_eventData[eventId].x = x;
    $gameMap._mk_eventData[eventId].y = y;
}

function removeFromEventData(eventId) {
    $gameMap._mk_eventData = $gameMap._mk_eventData || [ ];
    $gameMap._mk_eventData[eventId] = undefined;
}

MK.SpawnEvents.spawnFromXy = function(sourceMapName, xSource, ySource, xTarget, yTarget) {
    const events = fetchSourceEvents(sourceMapName);
    if (!events) return;

    const event = events.find(event => event && event.x == xSource && event.y == ySource);

    if (event) {
        return MK.SpawnEvents.spawn(sourceMapName, event.id, xTarget, yTarget);
    }
}

MK.SpawnEvents.spawnFromRectangle = function(sourceMapName, rect, xTarget, yTarget) {
    for (let x = 0; x < rect.width; x++) {
        for (let y = 0; y < rect.height; y++) {
            MK.SpawnEvents.spawnFromXy(
                sourceMapName,
                rect.x + x,
                rect.y + y,
                xTarget + x,
                yTarget + y,
            );
        }
    }
}

MK.SpawnEvents.spawnAtRegion = function(sourceMapName, sourceEventId, regionId, n) {
    for (let i = 0; i < n; i++) {
        const options = [ ];

        for (let x = 0; x < $gameMap.width(); x++) {
            for (let y = 0; y < $gameMap.height(); y++) {
                if (spawnPositionOk(regionId, x, y)) {
                    options.push({x, y});
                }
            }
        }
        const point = pickRandom(options);

        if (point) {
            MK.SpawnEvents.spawn(sourceMapName, sourceEventId, point.x, point.y);
        } else {
            break;
        }
    }
}

function spawnPositionOk(regionId, x, y) {
    return (
        regionId &&
        $gameMap.regionId(x, y) == regionId &&
        $gameMap.isPassable(x, y, 0) &&
        !$gameMap.eventIdXy(x, y)
    );
}

function pickRandom(list) {
    if (list.length) {
        const index = Math.floor(Math.random() * list.length);
        return list[index];
    }
}

MK.SpawnEvents.spawn = function(sourceMapName, sourceEventId, xTarget, yTarget) {
    const data = fetchEventData(sourceMapName, sourceEventId);
    if (!data) return;

    const nextEventId = getNextEventId();
    if (!nextEventId) {
        // to many events; can't spawn
        return;
    }

    resetEvent(nextEventId);
    persistEventInfo(nextEventId, sourceMapName, sourceEventId, xTarget, yTarget);
    pushEventData(nextEventId, data, xTarget, yTarget);

    const event = new Game_Event($gameMap.mapId(), nextEventId);
    event._sourceEventId = sourceEventId;
    event._sourceMapName = sourceMapName;
    event._pageIndex = -2;

    $gameMap.pushSpawnedEvent(nextEventId, event);
    event.refresh();
    addSpawnedEventToMapSpriteset(event);

    return event;
}

function fetchSourceEvents(sourceMapName) {
    sourceMapName = sourceMapName.replace('$', '').replace('!', '').trim().toLowerCase();
    return eventData[sourceMapName];
}

function fetchEventData(sourceMapName, sourceEventId) {
    sourceMapName = sourceMapName.replace('$', '').replace('!', '').trim().toLowerCase();
    return eventData[sourceMapName] && eventData[sourceMapName][sourceEventId];
}

function getNextEventId() {
    for (let id = 1; id < 10000; id++) {
        if (!$gameMap.event(id)) {
            return id;
        }
    }
}

function resetEvent(eventId) {
    resetEventEval.call(null, $gameMap.mapId(), eventId);
}

function persistEventInfo(eventId, sourceMapName, sourceEventId, x, y) {
    sourceMapName = sourceMapName.replace('$', '').replace('!', '').trim().toLowerCase();
    
    const eventInfo = {
        eventId,
        sourceMapName,
        sourceEventId,
        mapId: $gameMap.mapId(),
        x,
        y,
    };

    $gameMap._mk_persistedSpawnedEvents = $gameMap._mk_persistedSpawnedEvents || [ ];
    $gameMap._mk_persistedSpawnedEvents.push(eventInfo);
}

function removeFromPersistedSpawnedEvents(eventId) {
    $gameMap._mk_persistedSpawnedEvents = $gameMap._mk_persistedSpawnedEvents || [ ];
    
    $gameMap._mk_persistedSpawnedEvents = $gameMap._mk_persistedSpawnedEvents
        .filter(event =>
            event.mapId != $gameMap.mapId() ||
            event.eventId != eventId
        );
}

function addSpawnedEventToMapSpriteset(event) {
    const spriteset = SceneManager._scene._spriteset;
    const sprite = new Sprite_Character(event);
    
    spriteset._characterSprites.push(sprite);
    sprite.update();
    spriteset._tilemap.addChild(sprite);
}

MK.SpawnEvents.unspawnAll = function() {
    $gameMap.spawnedEventsMK().forEach(event => MK.SpawnEvents.unspawn(event.eventId()));
}

MK.SpawnEvents.unspawnAt = function(x, y) {
    $gameMap.events()
        .filter(event => event.x == x && event.y == y)
        .forEach(event => MK.SpawnEvents.unspawn(event.eventId()));
}

MK.SpawnEvents.unspawnFromRectangle = function(x, y, width, height) {
    const x2 = x + width;
    const y2 = y + height;
    
    $gameMap.events()
        .filter(event =>
            x <= event.x && event.x < x2 &&
            y <= event.y && event.y < y2
        )
        .forEach(event => MK.SpawnEvents.unspawn(event.eventId()));
}

MK.SpawnEvents.unspawnBySourceEventId = function(sourceMapName, sourceEventId) {
    sourceMapName = sourceMapName.replace('$', '').replace('!', '').trim().toLowerCase();

    $gameMap.events()
        .filter(event => event._sourceMapName == sourceMapName)
        .filter(event => event._sourceEventId == sourceEventId)
        .forEach(event => MK.SpawnEvents.unspawn(event.eventId()));
}

const _GameMap_eraseEvent = Game_Map.prototype.eraseEvent;
Game_Map.prototype.eraseEvent = function(eventId) {
    const spawnedEvent = this._mk_spawnedEvents ? this._mk_spawnedEvents[eventId] : undefined;

    if (spawnedEvent) {
        spawnedEvent.erase();
    } else {
        _GameMap_eraseEvent.call(this, eventId);
    }
}

const _GameEvent_erase = Game_Event.prototype.erase;
Game_Event.prototype.erase = function() {
    _GameEvent_erase.call(this);

    MK.SpawnEvents.unspawn(this.eventId());
}

MK.SpawnEvents.unspawn = function(eventId) {
    if (!$gameMap.event(eventId)) return;
    if (!getEventData(eventId)) return;

    _GameEvent_erase.call($gameMap.event(eventId));

    removeFromLocalSpawnedEvents(eventId);
    removeFromEventData(eventId);
    removeFromPersistedSpawnedEvents(eventId);
}

if (PluginManager && PluginManager.registerCommand) {

    PluginManager.registerCommand(PLUGIN_NAME, 'spawnEvent', args => {
        
        if ('fixed' == args.mode) {
            MK.SpawnEvents.spawn(
                args.sourceMapName,
                Number(args.eventId),
                Number(args.x),
                Number(args.y),
            );
        }
        if ('by Variables' == args.mode) {
            MK.SpawnEvents.spawn(
                args.sourceMapName,
                Number(args.eventId),
                $gameVariables.value(Number(args.variableX)),
                $gameVariables.value(Number(args.variableY)),
            );
        }
        if ('Player\'s Position' == args.mode) {
            MK.SpawnEvents.spawn(
                args.sourceMapName,
                Number(args.eventId),
                $gamePlayer.x,
                $gamePlayer.y,
            );
        }
    });

    PluginManager.registerCommand(PLUGIN_NAME, 'spawnEventAtRegion', args => {
        MK.SpawnEvents.spawnAtRegion(
            args.sourceMapName,
            Number(args.eventId),
            Number(args.regionId),
            Number(args.times),
        );
    });

    PluginManager.registerCommand(PLUGIN_NAME, 'unspawnAll', args => {
        MK.SpawnEvents.unspawnAll();
    });

    PluginManager.registerCommand(PLUGIN_NAME, 'unspawnBySourceEventId', args => {
        MK.SpawnEvents.unspawnBySourceEventId(
            args.sourceMapName,
            Number(args.eventId),
        );
    });
}

})();
