'use strict';

const uuid = require('uuid/v4');

const GameEntity = require('./GameEntity');
const ItemType = require('./ItemType');
const Logger = require('./Logger');
const Metadatable = require('./Metadatable');
const Player = require('./Player');
const { Inventory, InventoryFullError } = require('./Inventory');

/**
 * @property {Area}    area        Area the item belongs to (warning: this is not the area is currently in but the
 *                                 area it belongs to on a fresh load)
 * @property {object}  metadata    Essentially a blob of whatever attrs the item designer wanted to add
 * @property {Array}   behaviors   list of behaviors this object uses
 * @property {string}  description Long description seen when looking at it
 * @property {number}  id          vnum
 * @property {boolean} isEquipped  Whether or not item is currently equipped
 * @property {?Character} equippedBy Entity that has this equipped
 * @property {Map}     inventory   Current items this item contains
 * @property {string}  name        Name shown in inventory and when equipped
 * @property {?Room}   room        Room the item is currently in
 * @property {string}  roomDesc    Description shown when item is seen in a room
 * @property {string}  script      A custom script for this item
 * @property {ItemType|string} type
 * @property {string}  uuid        UUID differentiating all instances of this item
 * @property {boolean} closeable   Whether this item can be closed (Default: false, true if closed or locked is true)
 * @property {boolean} closed      Whether this item is closed
 * @property {boolean} locked      Whether this item is locked
 * @property {?entityReference} lockedBy Item that locks/unlocks this item
 * @property {?(Character|Item)} carriedBy Entity that has this in its Inventory
 *
 * @extends GameEntity
 */
class Item extends GameEntity {
  constructor (area, item) {
    super();
    const validate = ['keywords', 'name', 'id'];

    for (const prop of validate) {
      if (!(prop in item)) {
        throw new ReferenceError(`Item in area [${area.name}] missing required property [${prop}]`);
      }
    }

    this.area = area;
    this.metadata  = item.metadata || {};
    this.behaviors = new Map(Object.entries(item.behaviors || {}));
    this.defaultItems = item.items || [];
    this.description = item.description || 'Nothing special.';
    this.entityReference = item.entityReference; // EntityFactory key
    this.id          = item.id;

    this.maxItems    = item.maxItems || Infinity;
    this.initializeInventory(item.inventory, this.maxItems);

    this.isEquipped  = item.isEquipped || false;
    this.keywords    = item.keywords;
    this.name        = item.name;
    this.room        = item.room || null;
    this.roomDesc    = item.roomDesc || '';
    this.script      = item.script || null;

    if (typeof item.type === 'string') {
      this.type = ItemType[item.type] || item.type;
    } else {
      this.type = item.type || ItemType.OBJECT;
    }

    this.uuid        = item.uuid || uuid();
    this.closeable   = item.closeable || item.closed || item.locked || false;
    this.closed      = item.closed || false;
    this.locked      = item.locked || false;
    this.lockedBy    = item.lockedBy || null;

    this.carriedBy = null;
    this.equippedBy = null;
  }

  /**
   * Create an Inventory object from a serialized inventory
   * @param {object} inventory Serialized inventory
   */
  initializeInventory(inventory) {
    if (inventory) {
      this.inventory = new Inventory(inventory);
      this.inventory.setMax(this.maxItems);
    } else {
      this.inventory = null;
    }
  }

  hasKeyword(keyword) {
    return this.keywords.indexOf(keyword) !== -1;
  }

  /**
   * Add an item to this item's inventory
   * @param {Item} item
   */
  addItem(item) {
    this._setupInventory();
    this.inventory.addItem(item);
    item.carriedBy = this;
  }

  /**
   * Remove an item from this item's inventory
   * @param {Item} item
   */
  removeItem(item) {
    this.inventory.removeItem(item);

    // if we removed the last item unset the inventory
    // This ensures that when it's reloaded it won't try to set
    // its default inventory. Instead it will persist the fact
    // that all the items were removed from it
    if (!this.inventory.size) {
      this.inventory = null;
    }
    item.carriedBy = null;
  }

  /**
   * @return {boolean}
   */
  isInventoryFull() {
    this._setupInventory();
    return this.inventory.isFull;
  }

  _setupInventory() {
    if (!this.inventory) {
      this.inventory = new Inventory({
        items: [],
        max: this.maxItems
      });
    }
  }

  /**
   * Helper to find the game entity that ultimately has this item in their
   * Inventory in the case of nested containers. Could be an item, player, or
   * @return {Character|Item|null} owner
   */
  findCarrier() {
    let owner = this.carriedBy;

    while (owner) {
      if (!owner.carriedBy) {
        return owner;
      }

      owner = owner.carriedBy;
    }

    return null;
  }

  /**
   * Open a container-like object
   */
  open() {
    if (!this.closed) {
      return;
    }

    this.closed = false;
  }

  /**
   * Close a container-like object
   */
  close() {
    if (this.closed || !this.closeable) {
      return;
    }

    this.closed = true;
  }

  /**
   * Lock a container-like object
   */
  lock() {
    if (this.locked || !this.closeable) {
      return;
    }

    this.close();
    this.locked = true;
  }

  /**
   * Unlock a container-like object
   */
  unlock() {
    if (!this.locked) {
      return;
    }

    this.locked = false;
  }

  hydrate(state, serialized = {}) {
    if (this.__hydrated) {
      Logger.warn('Attempted to hydrate already hydrated item.');
      return false;
    }

    // perform deep copy if behaviors is set to prevent sharing of the object between
    // item instances
    if (serialized.behaviors) {
      const behaviors = JSON.parse(JSON.stringify(serialized.behaviors));
      this.behaviors = new Map(Object.entries(behaviors));
    }

    this.setupBehaviors(state.ItemBehaviorManager);

    this.description = serialized.description || this.description;
    this.keywords    = serialized.keywords || this.keywords;
    this.name        = serialized.name || this.name;
    this.roomDesc    = serialized.roomDesc || this.roomDesc;
    this.metadata = JSON.parse(JSON.stringify(serialized.metadata || this.metadata));
    this.closed = 'closed' in serialized ? serialized.closed : this.closed;
    this.locked = 'locked' in serialized ? serialized.locked : this.locked;

    if (typeof this.area === 'string') {
      this.area = state.AreaManager.getArea(this.area);
    }

    // if the item was saved with a custom inventory hydrate it
    if (this.inventory) {
      this.inventory.hydrate(state, this);
    } else {
    // otherwise load its default inv
      this.defaultItems.forEach(defaultItemId => {
        Logger.verbose(`\tDIST: Adding item [${defaultItemId}] to item [${this.name}]`);
        const newItem = state.ItemFactory.create(this.area, defaultItemId);
        newItem.hydrate(state);
        state.ItemManager.add(newItem);
        this.addItem(newItem);
      });
    }

    this.__hydrated = true;
  }

  serialize() {
    let behaviors = {};
    for (const [key, val] of this.behaviors) {
      behaviors[key] = val;
    }

    return {
      entityReference: this.entityReference,
      inventory: this.inventory && this.inventory.serialize(),

      // metadata is serialized/hydrated to save the state of the item during gameplay
      // example: the players a food that is poisoned, or a sword that is enchanted
      metadata: this.metadata,

      description: this.description,
      keywords: this.keywords,
      name: this.name,
      roomDesc: this.roomDesc,

      closed: this.closed,
      locked: this.locked,

      // behaviors are serialized in case their config was modified during gameplay
      // and that state needs to persist (charges of a scroll remaining, etc)
      behaviors,
    };
  }
}

module.exports = Item;