text: const Quest = require('./quest').Quest;
  const QuestType = require('./types');
  const Reward = require('./quest').QuestReward;
  const QuestObjective = require('./objectives').QuestObjective;
  const QuestObjectiveCompletionStatus = require('./objectives').QuestObjectiveCompletionStatus;
  const NPCFactory = require('./npcs').NPCFactory;
  const QuestNPC = require('./npcs').QuestNPC;
  
  const Scoreboard = require('scoreboard').Scoreboard;
  const assert = require('assert');
  const _ = require('underscore');
  const enderChest = require('ender-chest');
  const pf = require('pf');
  const Title = require('titles').Title;
  
  const completedPrefix = '\xA7a✔';
  const incompletePrefix = '\xA7c✘';
  const table = enderChest.getTable('quests');
  
  exports.Quest = Quest;
  exports.QuestType = QuestType;
  exports.QuestReward = Reward;
  exports.QuetObjective = QuestObjective;
  exports.QuestObjectiveCompletionStatus = QuestObjectiveCompletionStatus;
  exports.NPCFactory = NPCFactory;
  
  const userPermission = registerPermission('thiq.quests.user', true);
  
  function QuestFactory() {
      this.registeredQuests = {};
      this.activeQuests = {};
      this.completedQuests = {};
      this.npcFactory = new NPCFactory(this);
      loadQuests(this);
      loadStatuses(this);
      registerCommand({
          name: 'quests',
          description: 'Access to quest commands',
          usage: '\xA7cUsage: /<command> <addquest|removequest|addnpc|removenpc|setquestnpc>'
      }, (sender, label, args) => {
          assert(sender.hasPermission('thiq.quests.admin'), consts.defaultPermissionMessage);
          if (!args || args.length == 0) {
              sender.sendMessage('\xA7cUsage: /<command> <addquest|removequest|addnpc|removenpc|setquestnpc>');
              return;
          }
          let questName = '';
  
          if (args[0] == 'addquest') {
              for (var i = 1; i < args.length; i++) {
                  questName += args[i] + ' ';
              }
              var quest = new Quest(questName);
              try {
                  this.addQuest(quest);
                  sender.sendMessage(`\xA7dSuccessfully create quest ${questName}!`);
              } catch (ex) {
                  sender.sendMessage(`\xA7cFailed to add quest: ${ex.message}`);
              }
              return;
          } else if (args[0] == 'removequest') {
              for (var i = 1; i < args.length; i++) {
                  questName += args[i] + ' ';
              }
              try {
                  this.removeQuest(questName);
                  sender.sendMessage(`\xA7dSuccessfully removed quest ${questName}!`);
              } catch (ex) {
                  sender.sendMessage(`\xA7cFailed to remove quest: ${ex.message}`);
              }
              return;
          } else if (args[0] == 'removenpc') {
              return;
          } else if (args[0] == 'setquestnpc') {
              for (var i = 1; i < args.length; i++) {
                  questName += args[i] + ' ';
              }
              sender.sendMessage('\xA7dRight click the NPC this quest will be set to.');
              let questCheck = eventHandler('player', 'interactEntity', (e) => {
                  if (e.player != sender) return;
                  let npc = this.npcFactory.getNPC(e.getRightClicked());
                  e.cancelled = true;
                  if (!npc) {
                      sender.sendMessage('\xA7cA non-NPC entity was clicked. Cancelling assignment.');
                  } else {
                      let quest = this.registeredQuests[questName];
                      if (!quest) {
                          quest = new Quest(questName);
                          this.addQuest(quest);
                          sender.sendMessage(`\xA7dCould not locate quest "${questName}". Created a new one with that name anyways.`);
                      }
                      if (npc.hasQuest(questName)) {
                          sender.sendMessage('\xA7cThis NPC already has that quest assigned to it! Cancelling setup.');
                      } else {
                          npc.addQuest(questName);
                          sender.sendMessage(`\xA7dSuccessfully added ${questName} to ${npc.name}!`);
                          table.save();
                      }
                      
                  }
                  questCheck.unregister();
              });
          } else if (args[0] == 'getquests') {
              for (var field in this.registeredQuests) {
                  sender.sendMessage(`\xA7f${field}\r\n`);
              }
              return;
          } else if (args[0] == 'addnpc') {
              var npcName = args[1];
              var npcType = args[2]; // TODO implement
              if (this.npcFactory.hasNPC(npcName)) {
                  sender.sendMessage('\xA7cAn NPC with that name already exists!');
                  return;
              }
              var npc = this.createNPC(npcName, sender.getLocation());
              sender.sendMessage(`\xA7d${npcName} successfully spawned. Use /quests setquestnpc <your quest name> to set a quest!`);
              return;
          } else if (args[0] == 'addobj') {
              var questObjs = [];
              for (var i = 1; i < args.length; i++) {
                  // each obj is input as ACTION_COUNT_TYPE
                  var item = args[i];
                  if (/^\w{1,}_\d{1}_[a-z0-9]{1,}(:[0-9])?$/ig.test(item)) {
                      // we're reading an OBJ
                      var action = item.split('_')[0];
                      var count = item.split('_')[1];
                      var t = item.split('_')[2];
                      var questType = getQuestType(action);
                      var target = new QuestTarget(getTargetTypeFromAction(questType), t);
                      var questObj = new QuestObjective(questType, target, questObjs.length, parseInt(count));
                      questObjs.push(questObj);
                  } else {
                      // we're reading the quest name
                      questName += (item + ' ');
                  }
              }
              var quest = this.registeredQuests[questName];
              questObjs.forEach((obj) => {
                  quest.objectives.push(obj);
              });
              for (var field in this.activeQuests) {
                  var s = this.activeQuests[field];
                  if (!s) continue;
                  s.updateScoreboard();
              }
              table.set('quests', _.mapObject(this.registeredQuests, (val, key) => {
                  return val;
              }));
              sender.sendMessage('\xA7dSuccessfully added the objectives to the quest!');
              return;
          } else {
              sender.sendMessage(`\xA7cUnknown quest arg ${args[0]}: use <addquest|removequest|addnpc|removenpc|setquestnpc>`);
          }
      });
      eventHandler('player', 'join', (e) => {
          this.loadQuester(e.player);
          console.log(`Loaded quest status for ${e.player.getUniqueId()}`, 'e');
      });
      eventHandler('player', 'quit', (e) => {
          this.saveQuester(e.player);
          console.log(`Saved quests for ${e.player.getUniqueId()}`, 'e');
      });
  }
  
  QuestFactory.prototype.canPlayerQuest = function(player) {
      return player.hasPermission(userPermission) || player.isOp();
  }
  
  QuestFactory.prototype.createNPC = function(name, location, quests, type) {
      return this.npcFactory.createNPC(name, location, quests, type);
  }
  
  QuestFactory.prototype.addQuest = function(quest) {
      if (this.registeredQuests[quest.name]) throw new Error('A quest with that ID already exists');
      this.registeredQuests[quest.name] = quest;
      saveQuestsTable(this);
      return quest;
  }
  
  QuestFactory.prototype.removeQuest = function(questName) {
      if (!this.registeredQuests[questName]) throw new Error('A quest with the specified ID does not exist');
      this.registeredQuests[questName] = null;
      saveQuestsTable(this);
  }
  
  QuestFactory.prototype.addQuester = function(player, questName) {
      if (!this.registeredQuests[questName]) throw new Error('A quest with the specified ID does not exist');
      if (this.activeQuests[player.getUniqueId()] && !this.activeQuests[player.getUniqueId()].isCompleted)
          throw new Error('This player is currently embarked on a quest');
      else 
          this.activeQuests[player.getUniqueId()] = null;
      let status = new QuestStatus()
  }
  
  exports.QuestFactory = QuestFactory;
  
  function loadQuests(factory) {
      let existedQuests = table.get('quests') || {};
      let count = 0;
      for (var field in existedQuests) {
          var quest = existedQuests[field];
          var q = Quest.deserialize(quest);
          factory.registeredQuests[field] = q;
          count++;
      }
      console.log(`Loaded ${count} quests.`, 'd');
  }
  
  function loadStatuses(factory) {
      let onlinePlayers = Bukkit.getOnlinePlayers();
      for (var i = 0; i < onlinePlayers.length; i++) {
          var p = onlinePlayers[i];
          var status = QuestStatus.load(factory, p);
      }
  }
  
  function saveQuestsTable(factory) {
      var quests = [];
      for (var field in factory.registeredQuests) {
          quests.push(factory.registeredQuests[field]);
      }
      table.set('quests', quests);
  }
  
  function QuestStatus(factory, player, quest, compl) {
      this.factory = factory;
      this.player = player;
      this.completionStatus = compl || [];
      this.quest = quest;
      if (quest) beginWatch(this, quest);
      this.scoreboard = undefined;
  }
  
  QuestStatus.prototype.save = function() {
      var id = this.player.getUniqueId();
      var profile = pf.getProfile(id);
      profile.set('quests', 'current', this.serialize());
  }
  
  QuestStatus.prototype.serialize = function() {
      return {
          playerId: this.player.getUniqueId().toString(),
          objectives: this.completionStatus.map(o => {
              return { id: o.obj.id, count: o.count };
          })
      }
  }
  
  QuestStatus.prototype.updateObjStatus = function(obj) {
      let objText = obj.toString();
      if (obj.count >= obj.count) {
          this.scoreboard.setEntry(obj.obj.id, obj.count, `${completePrefix + objText}`);
      } else {
          this.scoreboard.setEntry(obj.obj.id, obj.count, `${incompletePrefix + objText}`);
      }
  }
  
  QuestStatus.prototype.updateScoreboard = function() {
      for (let i = 0; i < this.completionStatus.length; i++) {
          let obj = this.completionStatus[i];
          this.updateObjStatus(obj);
      }
  }
  
  QuestStatus.prototype.isComplete = function() {
      for (var i = 0; i < this.completionStatus.length; i++) {
          if (!this.completionStatus[i].isComplete()) return false;
      }
      return true;
  }
  
  QuestStatus.prototype.destroy = function() {
      delete(this.completionStatus);
      delete(this.scoreboard);
  }
  
  QuestStatus.deserialize = function(player, data, factory) {
      if (!data) return null;
      var status = new QuestStatus(factory, player);
      var quest = factory.registeredQuests[data.quest];
      var objectives = data.objectives;
      if (objectives) {
          for (var i = 0; i < objectives.length; i++) {
              var obj = objectives[i];
              var id = obj.id;
              var count = obj.count;
              var match = _.where(quest.objectives, { id: id })[0];
              var cStatus = new QuestObjectiveCompletionStatus(match, count || 0);
              status.completionStatus.push(cStatus);
          }
      }
      return status;
  }
  
  QuestStatus.load = function(factory, player) {
      var id = player.getUniqueId();
      var profile = pf.getProfile(id);
      var questData = profile.get('quests');
      if (!questData) return null; // it's a new player on the server
      var data = questData.current;
      if (!data) return null;
      var loadedStatus = QuestStatus.deserialize(player, data, factory);
      if (!loadedStatus) return null;
      var activeQuest = factory.registeredQuests[data.quest];
      if (!activeQuest) return null;
      factory.activeQuests[id] = loadedStatus;
      beginWatch(loadedStatus, activeQuest);
      return loadedStatus;
  }
  
  exports.QuestStatus = QuestStatus;
  
  function beginWatch(status, quest) {
      status.quest = quest.name;
      status.scoreboard = new Scoreboard(`\xA7e${quest.name}`);
      for (let i = 0; i < status.completionStatus.length; i++) {
          let status = status.completionStatus[i];
          let obj = status.completionStatus[i].obj;
          if (!obj || !obj.type) continue;
          let objText = status.toString();
          let score = status.scoreboard.addEntry(
              `${incompletePrefix}${objText}`, 
              0,
              obj.id);
          status.on('$progress', (sender, args) => {
              status.scoreboard.setEntry(obj.id, sender.count);
              status.save();
          });
          status.on('$completed', (sender, args) => {
              status.updateObjStatus(status);
              if (status.isComplete()) {
                  status.factory.completedQuests[status.player.getUniqueId()].push(status.quest);
              }
          });
          status.beginWatch(status.player);
          // we probably should call the "$completed" but if it's already there, chances are it was already called
          // and this could be used to get unlimited rewards from a quest
      }
      if (quest.showScoreboard) status.scoreboard.send(status.player);
  }