'use strict';

const Attributes = require('./Attributes');
const Character = require('./Character');
const CommandQueue = require('./CommandQueue');
const Config = require('./Config');
const Data = require('./Data');
const QuestTracker = require('./QuestTracker');
const Room = require('./Room');
const Logger = require('./Logger');
const PlayerRoles = require('./PlayerRoles');

/**
 * @property {Account} account
 * @property {number}  experience current experience this level
 * @property {string}  password
 * @property {string}  prompt     default prompt string
 * @property {net.Socket} socket
 * @property {QuestTracker} questTracker
 * @property {Map<string,function ()>} extraPrompts Extra prompts to render after the default prompt
 * @property {{completed: Array, active: Array}} questData
 * @extends Character
 */
class Player extends Character {
  constructor(data) {
    super(data);

    this.account = data.account || null;
    this.experience = data.experience || 0;
    this.extraPrompts = new Map();
    this.password  = data.password;
    this.prompt = data.prompt || '> ';
    this.socket = data.socket || null;
    const questData = Object.assign({
      completed: [],
      active: []
    }, data.quests);

    this.questTracker = new QuestTracker(this, questData.active, questData.completed);
    this.commandQueue = new CommandQueue();
    this.role = data.role || PlayerRoles.PLAYER;

    // Default max inventory size config
    if (!isFinite(this.inventory.getMax())) {
      this.inventory.setMax(Config.get('defaultMaxPlayerInventory') || 20);
    }
  }

  /**
   * @see CommandQueue::enqueue
   */
  queueCommand(executable, lag) {
    const index = this.commandQueue.enqueue(executable, lag);
    this.emit('commandQueued', index);
  }

  /**
   * Proxy all events on the player to the quest tracker
   * @param {string} event
   * @param {...*}   args
   */
  emit(event, ...args) {
    if (this.__pruned || !this.__hydrated) {
      return;
    }

    super.emit(event, ...args);

    this.questTracker.emit(event, ...args);
  }

  /**
   * Convert prompt tokens into actual data
   * @param {string} promptStr
   * @param {object} extraData Any extra data to give the prompt access to
   */
  interpolatePrompt(promptStr, extraData = {}) {
    let attributeData = {};
    for (const [attr, value] of this.attributes) {
      attributeData[attr] = {
        current: this.getAttribute(attr),
        max: this.getMaxAttribute(attr),
        base: this.getBaseAttribute(attr),
      };
    }
    const promptData = Object.assign(attributeData, extraData);

    let matches = null;
    while (matches = promptStr.match(/%([a-z\.]+)%/)) {
      const token = matches[1];
      let promptValue = token.split('.').reduce((obj, index) => obj && obj[index], promptData);
      if (promptValue === null || promptValue === undefined) {
        promptValue = 'invalid-token';
      }
      promptStr = promptStr.replace(matches[0], promptValue);
    }

    return promptStr;
  }

  /**
   * Add a line of text to be displayed immediately after the prompt when the prompt is displayed
   * @param {string}      id       Unique prompt id
   * @param {function ()} renderer Function to call to render the prompt string
   * @param {?boolean}    removeOnRender When true prompt will remove itself once rendered
   *    otherwise prompt will continue to be rendered until removed.
   */
  addPrompt(id, renderer, removeOnRender = false) {
    this.extraPrompts.set(id, { removeOnRender, renderer });
  }

  /**
   * @param {string} id
   */
  removePrompt(id) {
    this.extraPrompts.delete(id);
  }

  /**
   * @param {string} id
   * @return {boolean}
   */
  hasPrompt(id) {
    return this.extraPrompts.has(id);
  }

  /**
   * Move the player to the given room, emitting events appropriately
   * @param {Room} nextRoom
   * @param {function} onMoved Function to run after the player is moved to the next room but before enter events are fired
   * @fires Room#playerLeave
   * @fires Room#playerEnter
   * @fires Player#enterRoom
   */
  moveTo(nextRoom, onMoved = _ => _) {
    const prevRoom = this.room;
    if (this.room && this.room !== nextRoom) {
      /**
       * @event Room#playerLeave
       * @param {Player} player
       * @param {Room} nextRoom
       */
      this.room.emit('playerLeave', this, nextRoom);
      this.room.removePlayer(this);
    }

    this.room = nextRoom;
    nextRoom.addPlayer(this);

    onMoved();

    /**
     * @event Room#playerEnter
     * @param {Player} player
     * @param {Room} prevRoom
     */
    nextRoom.emit('playerEnter', this, prevRoom);
    /**
     * @event Player#enterRoom
     * @param {Room} room
     */
    this.emit('enterRoom', nextRoom);
  }

  save(callback) {
    if (!this.__hydrated) {
      return;
    }

    this.emit('save', callback);
  }

  hydrate(state) {
    super.hydrate(state);

    // QuestTracker has to be hydrated before the rest otherwise events fired by the subsequent
    // hydration will be emitted onto unhydrated quest objects and error
    this.questTracker.hydrate(state);

    if (typeof this.account === 'string') {
      this.account = state.AccountManager.getAccount(this.account);
    }

    // Hydrate inventory
    this.inventory.hydrate(state, this);

    // Hydrate equipment
    // maybe refactor Equipment to be an object like Inventory?
    if (this.equipment && !(this.equipment instanceof Map)) {
      const eqDefs = this.equipment;
      this.equipment = new Map();
      for (const slot in eqDefs) {
        const itemDef = eqDefs[slot];
        try {
          let newItem = state.ItemFactory.create(state.AreaManager.getArea(itemDef.area), itemDef.entityReference);
          newItem.initializeInventory(itemDef.inventory);
          newItem.hydrate(state, itemDef);
          state.ItemManager.add(newItem);
          this.equip(newItem, slot);
        } catch (e) {
          Logger.error(e.message);
        }
      }
    } else {
      this.equipment = new Map();
    }

    if (typeof this.room === 'string') {
      let room = state.RoomManager.getRoom(this.room);
      if (!room) {
        Logger.error(`ERROR: Player ${this.name} was saved to invalid room ${this.room}.`);
        room = state.AreaManager.getPlaceholderArea().getRoomById('placeholder');
      }

      this.room = room;
      this.moveTo(room);
    }
  }

  serialize() {
    let data = Object.assign(super.serialize(), {
      account: this.account.name,
      experience: this.experience,
      inventory: this.inventory && this.inventory.serialize(),
      metadata: this.metadata,
      password: this.password,
      prompt: this.prompt,
      quests: this.questTracker.serialize(),
      role: this.role,
    });

    if (this.equipment instanceof Map) {
      let eq = {};
      for (let [ slot, item ] of this.equipment) {
        eq[slot] = item.serialize();
      }
      data.equipment = eq;
    } else {
      data.equipment = null;
    }

    return data;
  }
}

module.exports = Player;