// MAZE_LocationMenu
// Copyright (c) 2020 MazeMaker, DarkPlasma

/**
 * 2021/01/11 1.1.2 場所移動コマンドを含まない移動先があった場合に例外を吐くように修正
 * 2020/12/31 1.1.1 対応する名前が存在しない場合に例外を吐くように修正
 *            1.1.0 訪れたことのないマップの色設定を追加
 * 2020/08/29 1.0.0 MZ対応
 */

/*: jp
 * @plugindesc ワールドマッププラグイン
 * @author MazeMaker, DarkPlasma
 * @license No License
 * @target MZ
 *
 * @base PluginCommonBase
 *
 * @param Unknown Text
 * @desc 場所に入れない場合の文章です。空欄でも大丈夫です。
 * @default ???
 * @type string
 *
 * @param Animation Rate
 * @desc マップのスクロールの速さ。
 * @default 20
 * @type number
 *
 * @param Use Help Window
 * @desc Window_Helpを使う設定。true = 使う、false = 使わない
 * @default true
 * @type boolean
 *
 * @param Help Window Width
 * @desc 説明ウィンドウの幅
 * @text 説明ウィンドウ幅
 * @type number
 * @default 816
 *
 * @param Display Only Enabled Location
 * @desc 既に行くことのできる場所のみ一覧に表示します
 * @text 有効な場所のみ表示
 * @type boolean
 * @default false
 *
 * @param Max Visible Location Window Rows
 * @desc 行き先ウィンドウの一画面に表示する最大数
 * @text 行き先表示最大数
 * @type number
 * @default 12
 *
 * @param newMapColor
 * @desc 訪れたことのないマップの色
 * @text 未踏マップ色
 * @type number
 * @default 2
 *
 * @command openLocationMenu
 * @text ワールドマップメニューを開く
 * @desc ワールドマップメニューを開きます
 * @arg locations
 * @text 行き先情報リスト
 * @type struct<Location>[]
 * @arg cursorEventId
 * @text カーソルイベントID
 * @desc カーソル画像のイベントのID。左上のリストで対象イベントの左側にある数字（先頭の0は不要）
 * @type number
 *
 * @plugindesc 場所を選ぶメニューを開ける体制。（世界マップ用など）
 * @help
 * version 1.1.0
 * プラグインコマンド: ワールドマップメニューを開く
 *
 * ※重要
 * 下記スクリプトコマンドによる制御は現在、非推奨です。
 * プラグインコマンドによる制御に移行してください。
 *
 * 非推奨:
 * mm.map.openMenu(スクリプトコマンド)
 * 例: mm.map.openMenu(["場所１","場所２","場所３"])
 * スクリプトコマンドに上記のように記入します。
 * "場所1"の名前と移動した場所の"名前"が一致していないと作動しません。
 * 場所の開放はスイッチで操作します。
 * "ON"で入れるようになり"OFF"で入れなくなります。
 *
 * <mapSwitch: スイッチ数>
 * 例: <mapSwitch: 1>
 * オプションとして、これは注釈コマンドの中を置いて
 * 場所が入れるか確認します。
 * ON　＝　入れる。　OFF　＝　入れない。
 * ※プラグインコマンドによる制御の場合、
 * プラグインコマンド側にスイッチを指定するため不要です。
 *
 * <mapDesc: 文章>
 * 例： <mapDesc: 怖い場所。昔ある大勇者が魔王を倒した。>
 * 注釈コマンドで入力します。
 * 移動先の場所説明です。
 * ※プラグインコマンドによる制御の場合、
 * プラグインコマンド側に説明を設定するため不要です。
 *
 * mm.map.doCursor()
 * カーソルのイベントを作ってから、
 * トリガーでは並列処理を使うとカーソルを作用します。
 * メニューの選択通じてカーソルが場所イベントのほうに
 * 動いてマップをスクロールします。
 * ※プラグインコマンドによる制御の場合、
 * プラグインコマンド側にカーソル画像イベントのIDを設定すれば不要です。
 */
/*~struct~Location:
 *
 * @param name
 * @text 場所名
 * @type string
 *
 * @param switchId
 * @text 有効化スイッチ
 * @type switch
 *
 * @param description
 * @text 説明文
 * @type string
 */

var mm = mm || [];
mm.map = mm.map || [];

(function ($) {
  "use strict";
  const pluginName = document.currentScript.src.replace(/^.*\/(.*).js$/, function () {
    return arguments[1];
  });
  const pluginParameters = PluginManager.parameters(pluginName);

  const settings = {
    unknownText: String(pluginParameters["Unknown Text"] || '???'),
    animationRate: Number(pluginParameters["Animation Rate"] || 20),
    useHelpWindow: String(pluginParameters["Use Help Window"] || 'true') === "true",
    helpWindowWidth: Number(pluginParameters["Help Window Width"] || 816),
    displayOnlyEnabledLocation: String(pluginParameters["Display Only Enabled Location"] || 'false') === 'true',
    maxVisibleLocationWindowRows: Number(pluginParameters["Max Visible Location Window Rows"] || 12),
    newMapColor: Number(pluginParameters.newMapColor || 2),
  };

  const COMMAND_NAME = {
    OPEN_LOCATION_MENU: 'openLocationMenu',
  };

  PluginManagerEx.registerCommand(document.currentScript, COMMAND_NAME.OPEN_LOCATION_MENU, args => {
    locationMenuManager.setCursorEventId(args.cursorEventId);
    locationMenuManager.openMenu(args.locations.map(location => {
      return new LocationMenu_Location(location.switchId, location.name, location.description);
    }));
  });

  const _Game_System_initialize = Game_System.prototype.initialize;
  Game_System.prototype.initialize = function () {
    _Game_System_initialize.call(this);
    this._visitedMapIds = [];
  };

  Game_System.prototype.visitedMapIds = function () {
    if (!this._visitedMapIds) {
      this._visitedMapIds = [];
    }
    return this._visitedMapIds;
  };

  Game_System.prototype.visitMap = function (mapId) {
    if (!this.visitedMapIds().includes(mapId)) {
      this.visitedMapIds().push(mapId);
    }
  };

  /**
   * 既に訪れたことのあるマップかどうか
   * @param {number} mapId マップID
   * @return {boolean}
   */
  Game_System.prototype.isAlreadySelectedMap = function (mapId) {
    return this.visitedMapIds().includes(mapId);
  };

  /**
   * return the first event with the given name
   * use at your own risk
   * @param {string} name イベント名
   * @return {Game_Event|undefined}
   */
  Game_Map.prototype.eventByName = function (name) {
    return this._events.find(event => event && event.event().name === name);
  };

  /**
   * @param {Game_Event} cursorEvent カーソルイベント
   */
  Game_Map.prototype.updateDisplayPositionByLocationMenu = function (cursorEvent) {
    const dispX = this.displayX() + this.screenTileX() * 0.6;
    const dispY = this.displayY() + this.screenTileY() / 2;
    const destX = cursorEvent.x;
    const destY = cursorEvent.y;
    const tweenX = dispX + ((destX - dispX) / settings.animationRate);
    const tweenY = dispY + ((destY - dispY) / settings.animationRate);
    this.setDisplayPos(tweenX - this.screenTileX() * 0.6, tweenY - this.screenTileY() / 2);
  };

  class Window_LocationMenu extends Window_ChoiceList {
    constructor() {
      super();
      this.initialize.apply(this, arguments);
    }

    /**
     * @param {LocationMenu_Location[]} locations 場所リスト
     * @param {number} defaultType デフォルト選択肢index
     * @param {Function} choiceCallback 選択時の挙動メソッド
     */
    initialize(locations, defaultType, choiceCallback) {
      if (!locations) {
        return;
      }
      this._locations = locations;
      this._locationNames = locations.map(location => location.displayName());
      this._defaultType = defaultType;
      this._cancelType = -1;
      this._positionType = 0;
      this._choicePositionType = 0;
      this._choiceCallback = choiceCallback;
      this._disables = locations.filterIndex(location => !location.isEnabled());
      this._newMaps = locations.filterIndex(location => location.isNewMap());
      super.initialize();
    }

    updatePlacement() {
      const positionType = this._choicePositionType;
      this.width = this.windowWidth();
      this.height = this.windowHeight();
      switch (positionType) {
        case 0:
          this.x = 0;
          break;
        case 1:
          this.x = (Graphics.boxWidth - this.width) / 2;
          break;
        case 2:
          this.x = Graphics.boxWidth - this.width;
          break;
      }
    }

    /**
     * @return {number}
     */
    maxChoiceWidth() {
      const maxWidth = 96;
      const locations = this._locationNames;
      if (!locations) {
        return maxWidth;
      }
      return locations.map(location => this.textSizeEx(location).width + this.itemPadding() * 2).concat([maxWidth])
        .reduce((a, b) => Math.max(a, b));
    }

    selectDefault() {
      this.select(this._defaultType);
    }

    makeCommandList() {
      const locations = this._locationNames;
      const disables = this._disables;
      if (!locations) {
        return;
      }
      locations.forEach(location => this.addCommand(location, 'choice'));
      disables.forEach(disable => this._list[disable].enabled = false);
    }

    numVisibleRows() {
      if (this._locationNames && this._locationNames.length < this.maxVisibleRows()) {
        return this._locationNames.length;
      }
      return this.maxVisibleRows();
    }

    maxVisibleRows() {
      return settings.maxVisibleLocationWindowRows;
    }

    callOkHandler() {
      this._choiceCallback(this.index())
      this.close();
    }

    commandName(index) {
      if (this._newMaps && this._newMaps.includes(index)) {
        return `\\C[${settings.newMapColor}]${super.commandName(index)}\\C[0]`;
      }
      return super.commandName(index);
    }

    locationName(index) {
      return this._locationNames[index];
    }

    currentMapId() {
      return this._locations[this.index()].mapId();
    }

    /**
     * キャンセル時は何もしない
     */
    callCancelHandler() {
    }

    processCancel() {

    }

    updateHelp() {
      super.updateHelp();
      const location = this._locations[this.index()];
      this._helpWindow.setText(location.description);
      this._helpWindow.open();
    }
  }

  const _Scene_Map_start = Scene_Map.prototype.start;
  Scene_Map.prototype.start = function () {
    _Scene_Map_start.call(this);
    $gameSystem.visitMap($gameMap.mapId());
  };

  const _Scene_Map_isMenuEnabled = Scene_Map.prototype.isMenuEnabled;
  Scene_Map.prototype.isMenuEnabled = function () {
    return !locationMenuManager.isSelecting && _Scene_Map_isMenuEnabled.call(this);
  };

  const _Scene_Map_update = Scene_Map.prototype.update;
  Scene_Map.prototype.update = function () {
    _Scene_Map_update.call(this);
    this.updateMapCursor();
  };

  Scene_Map.prototype.updateMapCursor = function () {
    locationMenuManager.doCursor();
  };

  const _Game_Player_canMove = Game_Player.prototype.canMove;
  Game_Player.prototype.canMove = function () {
    if (locationMenuManager.isSelecting) {
      return false;
    }
    return _Game_Player_canMove.call(this);
  };

  /**
   * 行き先情報
   */
  class LocationMenu_Location {
    constructor(switch_, name, description) {
      this._switch = switch_;
      this._name = name;
      this._description = description;
      this._mapId = null;
    }

    /**
     * @param {string} name マップ名
     * @return {LocationMenu_Location}
     */
    static fromName(name) {
      const event = $gameMap.eventByName(name);
      let _switch = null;
      let _description = null;
      if (!event) {
        throw `${name} に対応するイベントが存在しません。`;
      }
      if (event.page()) {
        event.page().list
          .filter(command => command.code === 108)
          .forEach(command => {
            const value = command.parameters[0].match(/<mapSwitch: (.*)>/i);
            if (value) {
              // create target object
              _switch = value[1];
            };
            const description = command.parameters[0].match(/<mapDesc: (.*)>/);
            if (description) {
              // create target object
              _description = description[1];
            };
          });
      }
      return new LocationMenu_Location(_switch, name, _description);
    }

    get name() {
      return this._name;
    }

    get description() {
      return this.isEnabled() ? this._description : "";
    }

    /**
     * @return {boolean} 選択可能であれば真
     */
    isEnabled() {
      return this._switch === 0 || $gameSwitches.value(this._switch);
    }

    /**
     * 以下の条件をすべて満たせば表示すべき対象
     * - 表示名が空文字列でない
     * - 対象となるイベントが存在する
     * - 有効な行き先のみ表示設定がOFF or 有効な行き先である
     * @return {boolean} 表示すべき対象であれば真
     */
    shouldBeDisplayed() {
      return this.displayName() !== "" &&
        !!this.event() &&
        (!settings.displayOnlyEnabledLocation || this.isEnabled());
    }

    /**
     * @return {string} 表示名
     */
    displayName() {
      return this.isEnabled() ? this._name : settings.unknownText;
    }

    /**
     * アンカーイベントを返す
     * @return {Game_Event|undefined}
     */
    event() {
      return $gameMap.eventByName(this._name);
    }

    /**
     * 移動先マップIDを返す
     * @return {number}
     */
    mapId() {
      if (this._mapId) {
        return this._mapId;
      }
      const event = this.event();
      const command = event.page().list
        .find(command => command.code === 201);
      if (!command) {
        throw `${this.name} イベントに、場所移動コマンドが含まれていません。`;
      }
      return command.parameters[0] === 0 ? command.parameters[1] : $gameVariables.value(command.parameters[1]);
    }

    /**
     * 新たに行けるようになったが、まだ行っていないマップであるか
     * @return {boolean}
     */
    isNewMap() {
      return this.isEnabled() && !$gameSystem.isAlreadySelectedMap(this.mapId());
    }
  }

  /**
   * ワールドマップメニュー制御
   */
  class LocationMenuManager {
    constructor() {
      /**
       * 行き先イベント一覧
       * @type {Game_Event[]}
       */
      this._locationEvents = [];

      /**
       * 行き先説明文ウィンドウ
       * @type {Window_Help|null}
       */
      this._helpWindow = null;

      /**
       * カーソルイベントのID
       * @type {number}
       */
      this._cursorEventId = 0;

      /**
       * @type {boolean}
       */
      this._isSelecting = false;

      /**
       * @type {Window_LocationMenu}
       */
      this._locationMenuWindow = null;
    }

    get isSelecting() {
      return this._isSelecting;
    }

    clear() {
      this._locationEvents = [];
    }

    /**
     * ワールドマップメニューを開く
     * @param {LocationMenu_Location[]} locations 行き先情報配列
     */
    openMenu(locations) {
      this.clear();
      this._locationEvents = locations.filter(location => location.shouldBeDisplayed()).map(location => location.event());
      this.createLocationMenuWindow(locations);
      this._isSelecting = true;

      this.createHelpWindow();
    }

    /**
     * ワールドマップメニューを開く
     * @deprecated
     * @param {string[]} locationNames 行き先名配列
     */
    openMenuLegacy(locationNames) {
      this.clear();

      const locations = locationNames.map(locationName => LocationMenu_Location.fromName(locationName));
      this._locationEvents = locations.filter(location => location.shouldBeDisplayed()).map(location => location.event());
      this.createLocationMenuWindow(locations);
      this._isSelecting = true;

      this.createHelpWindow();
    }

    /**
     * @param {LocationMenu_Location[]} locations 行き先一覧
     */
    createLocationMenuWindow(locations) {
      const displayLocations = locations.filter(location => location.shouldBeDisplayed());
      const defaultIndex = displayLocations.findIndex(location => {
        const event = location.event();
        return $gamePlayer.pos(event.x, event.y)
      });
      this._locationMenuWindow = new Window_LocationMenu(
        displayLocations,
        defaultIndex >= 0 ? defaultIndex : 0,
        this.decideTargetLocation.bind(this)
      );
      this.addWindowToScene(this._locationMenuWindow);
    }

    createHelpWindow() {
      // make help window
      if (settings.useHelpWindow) {
        const helpWindow = new Window_Help(this.helpWindowRect());
        this._helpWindow = helpWindow;
        this.addWindowToScene(helpWindow);
        this._locationMenuWindow.setHelpWindow(this._helpWindow);
        helpWindow.setText("");
        helpWindow.show();

        this._locationMenuWindow.start();
        this._locationMenuWindow.move(
          this._locationMenuWindow.x,
          helpWindow.height,
          this._locationMenuWindow.width,
          this._locationMenuWindow.height
        );
      }
    }

    helpWindowRect() {
      const wx = (Graphics.boxWidth - settings.helpWindowWidth) / 2;
      const wy = 0;
      const ww = settings.helpWindowWidth;
      const wh = Scene_Base.prototype.calcWindowHeight.call(null, 1, false);
      return new Rectangle(wx, wy, ww, wh);
    };

    /**
     * シーンにウィンドウを追加する
     * Scene_Mapであることを前提としており、ややダーティだが許容する
     * @param {Window_Base} additionalWindow ウィンドウ
     */
    addWindowToScene(additionalWindow) {
      SceneManager._scene.addWindow(additionalWindow);
    }

    /**
     * 行き先を決定する
     * @param {number} index 選択したindex
     */
    decideTargetLocation(index) {
      if (this._locationEvents) {
        const event = this._locationEvents[index];
        if (event) {
          event.start();
          $gameMap.setupStartingEvent();
        }
        this._isSelecting = false;
        this._locationEvents = null;
      }
    }

    /**
     * @param {number} id カーソルイベントID
     */
    setCursorEventId(id) {
      this._cursorEventId = id;
    }

    /**
     * カーソル移動を行う
     * @param {number} id カーソルイベントID
     */
    doCursor() {
      if (!this._locationMenuWindow) {
        return;
      }
      const index = this._locationMenuWindow.index();
      if (this._locationMenuWindow.active) {
        const choice = this._locationMenuWindow.locationName(index);
        const targetEvent = $gameMap.events().find(function (event) {
          return event.event().name === choice;
        });
        const cursor = $gameMap.event(this._cursorEventId);
        if (targetEvent) {
          cursor.setPosition(targetEvent.x, targetEvent.y);
        }
        $gameMap.updateDisplayPositionByLocationMenu(cursor);
      }
    }
  }

  let locationMenuManager = new LocationMenuManager();
  /**
   * 互換性維持
   */
  mm.map.openMenu = locationMenuManager.openMenuLegacy.bind(locationMenuManager);
  mm.map.doCursor = locationMenuManager.setCursorEventId.bind(locationMenuManager);

  Array.prototype.filterIndex = function(callback) {
    return [...Array(this.length).keys()].map(x => x).filter(index => callback(this[index]));
  };
})();
