Estiah - Mind Tricks

Collection of various extensions

// ==UserScript==
// @name           Estiah - Mind Tricks
// @namespace      SwataScripts
// @description    Collection of various extensions
// @include        https://www.estiah.com/*
// @exclude        https://www.estiah.com/
// @author         swata
// @version        111218.2
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_deleteValue
// @grant          GM_listValues
// @grant          GM_xmlhttpRequest
// @grant          GM_getResourceURL
// @grant          GM_addStyle
// @grant          GM_log
// ==/UserScript==

// TODO:
// - methods as camelCase
// - cardframe according to charm availability
// - buy directly
// - items needed for craft summary
// ? rewrite crafthelper to work on separate pages
// - track items from gathering
// - track items from fights
// - stars to event shops
// - stats gold/ap for dungeons/events/..

var dataVersion = "110301"; // before increasing this version, finish implementing Settings.Import
var debug = false;

/////////////////////////////
// general purpose classes //
/////////////////////////////

// storage

Storage = {};
Storage.LS = {};
Storage.LS.get = function (key) {
  return localStorage.getItem(Storage.LS._getStorageKey(key));
};

Storage.LS.set = function (key, value) {
  localStorage.setItem(Storage.LS._getStorageKey(key), value);
};

Storage.LS.remove = function (key) {
  localStorage.removeItem(Storage.LS._getStorageKey(key));
};

Storage.LS.list = function () {
  var ret = new Array();
  var length = unsafeWindow.localStorage.length - 1;
  for (i = 0; i <= length; i++) {
    var key = localStorage.key(i);
    if (key.indexOf(Storage.LS._keyPrefix) == 0)
      ret.push(Storage.LS._getInnerKey(key));
  }
  return ret;
};

Storage.LS._keyPrefix = "Mind_Tricks_";

Storage.LS._getStorageKey = function (key) {
  return Storage.LS._keyPrefix + key;
};

Storage.LS._getInnerKey = function (storageKey) {
  return storageKey.substr(Storage.LS._keyPrefix.length);
};

Storage.GM = {};
Storage.GM.get = GM_getValue;
Storage.GM.set = GM_setValue;
Storage.GM.remove = GM_deleteValue;
Storage.GM.list = GM_listValues;

Storage.useLS = function () {
  Storage.get = Storage.LS.get;
  Storage.set = Storage.LS.set;
  Storage.remove = Storage.LS.remove;
  Storage.list = Storage.LS.list;
  Storage.usingLS = true;
};

Storage.useGM = function () {
  Storage.get = Storage.GM.get;
  Storage.set = Storage.GM.set;
  Storage.remove = Storage.GM.remove;
  Storage.list = Storage.GM.list;
  Storage.usingLS = false;
};

Storage.foreach = function (callback) {
  var keys = Storage.list();
  for (var i = 0; i < keys.length; i++) {
    callback(keys[i]);
  }
};

Storage.switchToLS = function () {
  Storage.useLS();
  var keys = Storage.GM.list();
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    Storage.set(key, Storage.GM.get(key));
    Storage.GM.remove(key);
  }
};

Storage.switchToGM = function () {
  Storage.useGM();
  var keys = Storage.LS.list();
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    Storage.set(key, Storage.LS.get(key));
    Storage.LS.remove(key);
  }
};

Storage.init = function () {
  Storage.canUseLS = typeof localStorage != "undefined";
  Storage.canUseGM = typeof GM_listValues != "undefined";
  if (!Storage.canUseLS || (Storage.canUseGM && Storage.GM.list().length > 0))
    Storage.useGM();
  else Storage.useLS();
};

Storage.init();

// List

function List(init) {
  if (init == undefined || init.length < 2) this.array = new Array();
  else this.array = init.split(this.separator);
}

List.prototype.separator = ",";

List.prototype.add = function (item) {
  var add = !this.contains(item);
  if (add) this.array.push(item);
  //GM_log(item + ' ' + add);
  return add;
};

List.prototype.contains = function (item) {
  return this.array.indexOf(item) > -1;
};

List.prototype.remove = function (item) {
  var removed = this.array.splice(this.array.indexOf(item), 1);
  return removed != undefined;
};

List.prototype.toString = function () {
  //GM_log('save! ' + this.array.join(this.separator));
  return this.array.join(this.separator);
};

List.prototype.foreach = function (callback) {
  for (var key = 0; key < this.array.length; key++) {
    callback(this.array[key]);
  }
};

List.prototype.count = function () {
  return this.array.length;
};

// Map (acts as multimap too - just use add() instead of insert() and remove(x,y) instead of remove(x))

function Map(init, mainSeparator, semiSeparator) {
  this.container = new Object();
  if (mainSeparator != undefined) this.mainSeparator = mainSeparator;
  if (semiSeparator != undefined) this.semiSeparator = semiSeparator;
  if (init != undefined) {
    if (init.length < 2) return;
    var pairs = init.split(this.mainSeparator);
    for (var key = 0; key < pairs.length; key++) {
      var items = pairs[key].split(this.semiSeparator);
      this.container[items[0]] = items.slice(1);
    }
  }
}

Map.prototype.mainSeparator = ",,";
Map.prototype.semiSeparator = ";;";

Map.prototype.insert = function (val1, val2, val3, val4) {
  var val = null;
  if ((val3 == undefined || val3 === "") && (val4 == undefined || val4 === ""))
    val = [val2];
  else if (val4 == undefined || val4 === "") val = [val2, val3];
  else val = [val2, val3, val4];
  if (this.container[val1] == val) return false;
  this.container[val1] = val;
  return true;
};

Map.prototype.add = function (val1, val2) {
  if (val1 == undefined || val2 == undefined || val1 == "" || val2 == "")
    return false;
  if (this.container[val1] == undefined) {
    this.container[val1] = [val2];
    return true;
  }
  if (this.container[val1].indexOf(val2) > -1) return false; // already there
  this.container[val1].push(val2);
  return true;
};

Map.prototype.contains = function (val1, val2) {
  var values = this.container[val1];
  if (values == undefined) return false;
  if (val2 == undefined) return true;
  return values.indexOf(val2) > -1;
};

Map.prototype.remove = function (val1, val2) {
  var values = this.container[val1];
  if (values == undefined) return false;
  if (val2 == undefined) {
    delete this.container[val1];
    return true;
  }
  var i = values.indexOf(val2);
  if (i == -1) return false;
  if (values.length == 1)
    // last - delete whole item
    delete this.container[val1];
  else values.splice(i, 1);
  return true;
};

Map.prototype.get = function (val1) {
  var ret = this.container[val1];
  if (ret == undefined) return [];
  return ret;
};

//Map.prototype.GetKeys

Map.prototype.toString = function () {
  var res = new Array();
  for (var key in this.container) {
    res.push(
      key + this.semiSeparator + this.container[key].join(this.semiSeparator)
    );
  }
  return res.join(this.mainSeparator);
};

Map.prototype.foreach = function (callback) {
  for (var key in this.container) callback(key, this.container[key]);
};

///////////////////////////////////////////////
// Containers around value stored in browser //
///////////////////////////////////////////////

// GMList

function GMList(gmName, autoupdate, clear) {
  this.name = gmName;
  this.autoupdate = autoupdate != undefined && autoupdate;
  if (clear != undefined && clear) this.list = new List();
  else this.list = new List(Storage.get(gmName));
}

GMList.prototype.add = function (val) {
  var added = this.list.add(val);
  if (added && this.autoupdate) this.save();
  return added;
};

GMList.prototype.contains = function (val) {
  return this.list.contains(val);
};

GMList.prototype.remove = function (val) {
  var removed = this.list.remove(val);
  if (removed && this.autoupdate) this.save();
  return removed;
};

GMList.prototype.save = function () {
  Storage.set(this.name, this.list.toString());
};

GMList.prototype.foreach = function (callback) {
  this.list.foreach(callback);
};

GMList.prototype.count = function () {
  return this.list.count();
};

// GMMap

function GMMap(gmName, autoupdate, clear) {
  this.name = gmName;
  this.autoupdate = autoupdate != undefined && autoupdate;
  if (clear != undefined && clear) this.map = new Map(undefined);
  else this.map = new Map(Storage.get(gmName));
}

GMMap.prototype.insert = function (val1, val2, val3, val4) {
  var added = this.map.insert(val1, val2, val3, val4);
  if (added && this.autoupdate) this.save();
  return added;
};

GMMap.prototype.add = function (val1, val2) {
  var added = this.map.add(val1, val2);
  if (added && this.autoupdate) this.save();
  return added;
};

GMMap.prototype.remove = function (val1, val2) {
  var removed = this.map.remove(val1, val2);
  if (removed && this.autoupdate) this.save();
  return removed;
};

GMMap.prototype.contains = function (val1, val2) {
  return this.map.contains(val1, val2);
};

GMMap.prototype.get = function (val1) {
  return this.map.get(val1);
};

GMMap.prototype.save = function () {
  Storage.set(this.name, this.map.toString());
};

GMMap.prototype.foreach = function (callback) {
  this.map.foreach(callback);
};

///////////////////////////////////////////////
// General web-manipulating helper functions //
///////////////////////////////////////////////

Web = {};
Web.HttpRequest = function (url, onLoad) {
  if (debug) GM_log('fetching "' + url + '"');
  GM_xmlhttpRequest({
    method: "GET",
    url: url,
    onload: function (responseDetails) {
      var parser = new DOMParser();
      var responseXML = parser.parseFromString(
        Web.SanitizeResponse(responseDetails.responseText),
        "text/xml"
      );
      if (debug) {
        var title = responseXML.getElementsByTagName("title")[0];
        GM_log(
          'url "' +
            url +
            '" loaded with ' +
            responseDetails.responseText.length +
            ' characters, title "' +
            title.innerHTML +
            '"'
        );
      }
      onLoad(responseXML, url);
      Launcher.NewPageFetched(url, responseXML);
    },
  });
};

Web.SanitizeResponse = function (text) {
  return text
    .replace(/></g, "> <") // auto closing tags
    .replace(" & ", " and ") // Hack & Slash not encoded
    .replace(
      new RegExp("<strong>([0-9]+)</span> </strong>", "g"),
      "<strong>$1</strong> </span>"
    ); // switch </span and </strong>
};

Web.SwitchVisibility = function (showClass, hide) {
  var show = hide.parentNode.getElementsByClassName(showClass);
  for (var d = 0; d < show.length; d++) show[d].style.display = "";
  hide.style.display = "none";
};

Web.CreateElement = function (type, className, parent) {
  //GM_log('Web.CreateElement' + type + ' ' + className + ' ' + parent);
  var element = document.createElement(type);
  if (className != undefined) element.setAttribute("class", className);
  if (parent != undefined) parent.appendChild(element);
  return element;
};

Web.appendText = function (text, parent) {
  parent.appendChild(document.createTextNode(text));
};

Web.AddButton = function (label, href, type, func, root, decision) {
  var c = "button button_c" + type;
  if (decision == true) c += " decision";
  var div = Web.CreateElement("div", c, root);
  Web.AddInnerLink(label, href, "button_inner button_ic" + type, func, div);
  return div;
};

Web.AddInnerLink = function (label, href, className, func, root) {
  var a = Web.CreateElement("a", className, root);
  Web.appendText(label, a);
  a.setAttribute("href", href);
  a.setAttribute("onclick", "return false;");
  a.addEventListener("click", func, true);
  return a;
};

Web.CreatePageTitle = function (title, desc, wireFrame, page) {
  var titleFrame = Web.CreateElement(
    "div",
    "wireframe_" + wireFrame + " common_content common_wl",
    page
  );
  var titleEl = Web.CreateElement("div", "section_title c2", titleFrame);
  titleEl.innerHTML = title;
  var descEl = Web.CreateElement("div", "section_text", titleFrame);
  if (desc != undefined) Web.appendText(desc, descEl);
  return titleFrame;
};

Web.CreatePage = function (title, desc, wireFrame, className, cloneClass) {
  var cloneFrom = document.getElementsByClassName(cloneClass)[0];
  var page = cloneFrom.cloneNode(false);
  cloneFrom.parentNode.appendChild(page);
  Web.CreatePageTitle(title, desc, wireFrame, page);
  page.setAttribute("class", cloneClass + " " + className);
  page.style.display = "none";
  return page;
};

Web.removeNode = function (node) {
  node.parentNode.removeChild(node);
};

Web.AddAsFirstChild = function (what, parent) {
  parent.insertBefore(what, parent.firstChild);
};

Web.MouseClick = function (element) {
  var evt = element.ownerDocument.createEvent("MouseEvents");
  evt.initMouseEvent(
    "click",
    true,
    true,
    element.ownerDocument.defaultView,
    1,
    0,
    0,
    0,
    0,
    false,
    false,
    false,
    false,
    0,
    null
  );
  element.dispatchEvent(evt);
};

Web.CreateMouseClickFunction = function (element) {
  return function () {
    Web.MouseClick(element);
  };
};

Web.FindBrAfter = function (element) {
  return Web.FindTagAfter(element, "BR");
};

Web.FindTagAfter = function (element, tagname) {
  var el = element.nextSibling;
  while (el != undefined && el.tagName != tagname) el = el.nextSibling;
  return el;
};

Web.GetLastPartOfHref = function (a) {
  return a.href.substring(a.href.lastIndexOf("/") + 1);
};

Web.SanitizeXpathName = function (inp) {
  if (inp.indexOf('"') > -1) {
    var inParts = inp.split('"');
    var outParts = [];
    for (var i = 0; i < inParts.length; i++) {
      if (inParts[i] != "") outParts.push('"' + inParts[i] + '"');
      if (i < inParts.length - 1) outParts.push("'" + '"' + "'");
    }
    return "concat(" + outParts.join(",") + ")";
  } else return '"' + inp + '"';
};

///////////////////////////////////////
// Functions related to Estiah pages //
///////////////////////////////////////

Estiah = {};
Estiah.GetCityName = function () {
  var cityElement = document.evaluate(
    '//a[@href="/city" and contains(@class, "nolink")]',
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  if (cityElement == null) return null;
  return cityElement.innerHTML.substring(5, 8); // return abbreviation
};

Estiah.CurrentCity = Estiah.GetCityName();

Estiah.GetPlayerId = function () {
  var archievementsElement = document.evaluate(
    '//div[contains(@class, "wireframe_menu")]//a[contains(@href, "/character/achievement/index/id/")]',
    document,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  if (archievementsElement == null) return null;
  return archievementsElement.getAttribute("href").substring(32);
};

Estiah.GetPlayerLevel = function () {
  var level = document.getElementsByClassName("PT_update_level");
  if (level.length > 0) return parseInt(level[0].innerHTML);
  return 1;
};

Estiah.Player = {};

Estiah.Player.Id = Estiah.GetPlayerId();

Estiah.Player.Level = Estiah.GetPlayerLevel();

Estiah.GetShopIdFromUrl = function (url) {
  var id = url.substring(url.indexOf("/shop/") + 6);
  if (id.indexOf("/") > -1) {
    var start = id.indexOf("/") + 1;
    id = id.substring(start, id.indexOf("/", start));
  }
  return id;
};

Estiah.GetItemId = function (item) {
  return item.getAttribute("href").substring(6); // href="/item/xxxx"
};

Estiah.IsShopUrl = function (url) {
  return url.indexOf("/shop/") > -1;
};

Estiah.IsItemShopUrl = function (url) {
  if (!Estiah.IsShopUrl(url)) return false;
  var shopType = Estiah.GetShopIdFromUrl(url)[0];
  return shopType == "1" || shopType == "2";
};

Estiah.IsVendorShopUrl = function (url) {
  if (!Estiah.IsShopUrl(url)) return false;
  var shopType = Estiah.GetShopIdFromUrl(url)[0];
  return shopType == "3";
};

Estiah.IsCraftShopUrl = function (url) {
  if (!Estiah.IsShopUrl(url)) return false;
  var shop = Estiah.GetShopIdFromUrl(url);
  return (
    shop[0] == "4" || shop[0] == "5" || shop[0] == "6" || shop == "tracked"
  );
};

Estiah.Market = {};

Estiah.Market.AddShopGroup = function (ico, text, root) {
  var label = Web.CreateElement("div", "stand_type lhp", root);
  var labelFloat = Web.CreateElement("div", "floating", label);
  var icon = Web.CreateElement("div", "icon icon_" + ico, labelFloat);
  Web.appendText(text, label);
  var standList = Web.CreateElement("div", "standlist", root);
  return standList;
};

Estiah.Market.AddShop = function (
  shopId,
  className,
  shopName,
  shopDesc,
  action,
  root
) {
  var stand = Web.CreateElement("div", "stand", root);
  var name = Web.CreateElement("div", "name bd1", stand);
  Web.AddInnerLink(shopName, "/shop/" + shopId, className, action, name);
  var desc = Web.CreateElement("div", "description", stand);
  Web.appendText(shopDesc, desc);
  return stand;
};

Estiah.Fetcher = {};

Estiah.Fetcher._cache = function (key, callback, getValue) {
  //GM_log('getting ' + key);
  if (Estiah.Fetcher[key] != undefined)
    Estiah.Fetcher._runCallback(key, callback);
  else {
    var callbackKey = key + "_callback";
    if (Estiah.Fetcher[callbackKey] != undefined)
      Estiah.Fetcher[callbackKey].push(callback);
    else {
      Estiah.Fetcher[callbackKey] = new Array();
      Estiah.Fetcher[callbackKey].push(callback);
      getValue(function () {
        //GM_log('actually retrieving ' + key);
        Estiah.Fetcher[key] = arguments;
        Estiah.Fetcher._runCallbacks(key);
      });
    }
  }
};

Estiah.Fetcher._runCallbacks = function (key) {
  var callbackArray = Estiah.Fetcher[key + "_callback"];
  while (callbackArray.length > 0)
    Estiah.Fetcher._runCallback(key, callbackArray.pop());
};

Estiah.Fetcher._runCallback = function (key, callback) {
  var args = Estiah.Fetcher[key];
  callback(args[0], args[1]);
};

Estiah.Fetcher.SynchronizeCallbacks = function (
  syncKey,
  callback,
  func1,
  func2
) {
  func1(function (data) {
    Estiah.Fetcher[syncKey + "_data1"] = data;
    Estiah.Fetcher._checkFinished(syncKey, callback);
  });
  func2(function (data) {
    Estiah.Fetcher[syncKey + "_data2"] = data;
    Estiah.Fetcher._checkFinished(syncKey, callback);
  });
};

Estiah.Fetcher._checkFinished = function (syncKey, callback) {
  if (
    Estiah.Fetcher[syncKey + "_data1"] != undefined &&
    Estiah.Fetcher[syncKey + "_data2"] != undefined
  )
    callback(
      Estiah.Fetcher[syncKey + "_data1"],
      Estiah.Fetcher[syncKey + "_data2"]
    );
};

Estiah.Fetcher.FetchItemAndCharmCount = function (callback) {
  var key = "_ItemCountAndCharmCount";
  Estiah.Fetcher._cache(key, callback, function (innerCallback) {
    Estiah.Fetcher.SynchronizeCallbacks(
      key,
      innerCallback,
      Estiah.Inventory.FetchItemCounts,
      Estiah.Charms.FetchCharmCounts
    );
  });
};

Estiah.Inventory = {};

Estiah.Inventory.UrlTest = function (url) {
  return (
    Launcher.UrlTest.EndsWith("/character/inventory")(url) ||
    Launcher.UrlTest.Contains("/character/inventory/index/sort/")(url)
  );
};

Estiah.Inventory.FetchItemCounts = function (callback) {
  Web.HttpRequest("http://www.estiah.com/character/inventory", function (doc) {
    callback(Estiah.Inventory._getItemCounts(doc));
  });
};

Estiah.Inventory._getItemCounts = function (doc) {
  var map = new Object();
  var items = doc.evaluate(
    '//*[starts-with(@id,"StorageItemCount")]',
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < items.snapshotLength; i++)
    map[items.snapshotItem(i).id.substring(16)] = parseInt(
      items.snapshotItem(i).innerHTML
    );

  items = doc.evaluate(
    '//*[starts-with(@id,"InventoryItemCount")]',
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < items.snapshotLength; i++) {
    var id = items.snapshotItem(i).id.substring(18);
    if (map[id] != undefined)
      map[id] = map[id] + parseInt(items.snapshotItem(i).innerHTML);
    else map[id] = parseInt(items.snapshotItem(i).innerHTML);
  }
  return map;
};

Estiah.Charms = {};

Estiah.Charms.FetchCharmCounts = function (callback) {
  Estiah.Fetcher.SynchronizeCallbacks(
    "_CharmCount",
    function (map1, map2) {
      for (var i in map2) map1[i] = map2[i];
      callback(map1);
    },
    Estiah.Charms._fetchCharmCounts("http://www.estiah.com/character/card"),
    Estiah.Charms._fetchCharmCounts(
      "http://www.estiah.com/character/card/index/collection/archive"
    )
  );
};

Estiah.Charms._fetchCharmCounts = function (url) {
  return function (callback) {
    Web.HttpRequest(url, function (doc) {
      callback(Estiah.Charms._getCharmCounts(doc));
    });
  };
};

Estiah.Charms._getCharmCounts = function (doc) {
  var map = new Object();
  var counts = doc.getElementsByClassName("count");
  for (var c = 0; c < counts.length; c++) {
    var charmId = Estiah.Charms._getCharmIdFromCountEl(counts[c]);
    map[charmId] = counts[c].innerHTML; // not a problem - charm is either in one or in other collection
  }
  return map;
};

Estiah.Charms.GetCharmId = function (dom) {
  var counts = dom.getElementsByClassName("count");
  if (counts.length == 0) return undefined;
  return Estiah.Charms._getCharmIdFromCountEl(counts[0]);
};

Estiah.Charms._getCharmIdFromCountEl = function (el) {
  var cl = el.getAttribute("class"); // 'count PT_update_count_t1i547'
  return cl.substr(cl.indexOf("PT_update_count_t1i") + 19);
};

//////////////
// Launcher //
//////////////

Launcher = {};
Launcher.read = new Array();
Launcher.modify = new Array();
Launcher.Registrator = function (id) {
  this.id = id;
};
Launcher.Registrator.prototype.Read = function (checkfunc, runfunc) {
  Launcher.read.push([this.id, checkfunc, runfunc]);
  return this;
};
Launcher.Registrator.prototype.Modify = function (checkfunc, runfunc) {
  Launcher.modify.push([this.id, checkfunc, runfunc]);
  return this;
};
Launcher.Registrator.prototype.UpdateData = function (version, runfunc) {
  DataUpdate.UpdateData(version, runfunc);
};
Launcher.Register = function (id, text) {
  Settings.texts.insert(id, text);
  return new Launcher.Registrator(id);
};

Launcher.UrlTest = {};
Launcher.UrlTest.EndsWith = function (suffix) {
  return function (url) {
    return url.substr(-suffix.length) == suffix;
  };
};
Launcher.UrlTest.Contains = function (str) {
  return function (url) {
    return url.indexOf(str) >= 0;
  };
};
Launcher.UrlTest.EveryPage = function (url, doc) {
  return doc.URL !== undefined && !Launcher.UrlTest.LogIn(url);
};
Launcher.UrlTest.LogIn = function (url) {
  return Launcher.UrlTest.Contains("user/auth")(url);
};

Launcher.Run = function (array, url, doc) {
  for (var key = 0; key < array.length; key++) {
    // do something
    if (Settings.IsEnabled(array[key][0]) && array[key][1](url, doc))
      array[key][2](doc, url);
  }
};

Launcher.NewPageFetched = function (url, doc) {
  Launcher.Run(Launcher.read, url, doc);
};

Launcher.NewPageCreated = function (url, doc) {
  Launcher.Run(Launcher.modify, url, doc);
};

// run it after everything is registered
Launcher.Launch = function () {
  DataUpdate.SetDataVersion();
  Launcher.NewPageFetched(document.URL, document);
  Settings.Attach();
  Launcher.NewPageCreated(document.URL, document);
};

/////////////////
// Data Update //
/////////////////

DataUpdate = {};
DataUpdate.SetDataVersion = function () {
  Storage.set("DataVersion", dataVersion);
};

DataUpdate.UpdateData = function (version, runfunc) {
  var dataver = Storage.get("DataVersion");
  if (dataver == undefined || dataver < version) runfunc();
};

DataUpdate.RemoveEmptyValues = function () {
  GM_log("running empty values check ..");
  Storage.foreach(function (sKey) {
    if (sKey.substr(0, 9) == "NewCharms") return;
    var map = new GMMap(sKey);
    map.foreach(function (key, valueArray) {
      while (valueArray.indexOf("") != -1) {
        GM_log(
          "removing empty value in map " +
            sKey +
            ", key " +
            key +
            ", data " +
            valueArray
        );
        valueArray.splice(valueArray.indexOf(""), 1);
      }
      if (valueArray.length == 0) map.remove(key);
    });
    var newValue = map.map.toString();
    var oldValue = Storage.get(sKey);
    if (newValue != oldValue) {
      GM_log("replacing value of " + sKey);
      GM_log("old: " + oldValue);
      GM_log("new: " + newValue);
      map.save();
    }
  });
};

DataUpdate.UpdateData("100927", DataUpdate.RemoveEmptyValues);

//////////////
// Settings //
//////////////

Settings = {};
Settings.Addons = new GMMap("Addons", true);
Settings.IsEnabled = function (addon) {
  return Settings.Addons.get(addon).length > 0;
};
Settings.texts = new Map();

Settings.SettingsShopName = "Swata's Mind Tricks";
Settings.SettingsShopDesc =
  "Young man is showing off wonderful mind tricks. Maybe you can learn few of them too ..";
Settings.ImportShopName = "Soul Harvester's Tent";
Settings.ImportShopDesc =
  "This strange man can store your memory into a crystal or back. Be aware, it hurts ..";

Settings.AlterMarket = function (doc) {
  var shopRoots = doc.evaluate(
    '//div[contains(@class, "wireframe_market")]/div[contains(@class, "format") and contains(@class, "section_text")]',
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < shopRoots.snapshotLength; i++) {
    var shopRoot = shopRoots.snapshotItem(i);
    var shopGroup = Estiah.Market.AddShopGroup(
      "staff",
      "Dark corner",
      shopRoot
    );
    Estiah.Market.AddShop(
      "settings",
      "item",
      Settings.SettingsShopName,
      Settings.SettingsShopDesc,
      Settings.ShowSettings,
      shopGroup
    );
    Estiah.Market.AddShop(
      "import",
      "item",
      Settings.ImportShopName,
      Settings.ImportShopDesc,
      Settings.ShowImport,
      shopGroup
    );
  }
  var cityShops = doc.getElementsByClassName("wireframe_2")[0];
  cityShops.setAttribute(
    "class",
    cityShops.getAttribute("class") + " CityShops"
  );
};

Settings.BackToMarket = function () {
  var settings = this.parentNode.parentNode.parentNode.parentNode;
  Web.SwitchVisibility("CityShops", settings);
  settings.parentNode.removeChild(settings);
  scroll(0, 0);
};

Settings.ShowSettings = function () {
  var thisPage =
    this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
  var shopsRoot = thisPage.parentNode;
  Settings.CreateSettingsPage();
  Web.SwitchVisibility("Settings", thisPage);
  scroll(0, 0);
};

Settings.CreateSettingsPage = function () {
  var settingsPage = Web.CreatePage(
    Settings.SettingsShopName,
    Settings.SettingsShopDesc,
    "market",
    "Settings",
    "wireframe_2"
  );
  var ao;
  if (Storage.canUseLS && Storage.canUseGM) {
    ao = Web.CreateElement(
      "div",
      "wireframe_market common_content common_wl",
      settingsPage
    );
    var aoTitle = Web.CreateElement("div", "paragraph_title c2", ao);
    Web.appendText("Data in:\u00A0", aoTitle);
    var storage = Web.CreateElement("span", "", aoTitle);
    storage.id = "storage";
    var href = "/settings/" + "DataStorage";
    var aoUseLS = Web.AddButton(
      "Use localStorage",
      href,
      3,
      Settings.UseLS,
      ao
    );
    var aoUseGM = Web.AddButton(
      "Use greasemonkeyStorage",
      href,
      3,
      Settings.UseGM,
      ao
    );
    if (Storage.usingLS) {
      storage.innerHTML = "localStorage";
      aoUseLS.style.display = "none";
    } else {
      storage.innerHTML = "greasemonkeyStorage";
      aoUseGM.style.display = "none";
    }
    Web.appendText("\u00A0", aoTitle);
    var aoDesc = Web.CreateElement("div", "paragraph_text", ao);
    Web.appendText(
      "Here you can change where the data is stored. It may take a while to transfer all the data. Do not click if you do not know what you are doing.",
      aoDesc
    );
  }
  Settings.texts.foreach(function (addonName, addonDesc) {
    ao = Web.CreateElement(
      "div",
      "wireframe_market common_content common_wl",
      settingsPage
    );
    var aoTitle = Web.CreateElement("div", "paragraph_title c2", ao);
    Web.appendText(addonName + "\u00A0", aoTitle);
    var href = "/settings/" + addonName;
    var aoEnable = Web.AddButton("Enable", href, 2, Settings.Enable, ao);
    var aoDisable = Web.AddButton("Disable", href, 3, Settings.Disable, ao);
    if (Settings.IsEnabled(addonName)) aoEnable.style.display = "none";
    else aoDisable.style.display = "none";
    var aoDesc = Web.CreateElement("div", "paragraph_text", ao);
    Web.appendText(addonDesc[0], aoDesc);
  });

  var buttonsRoot = Web.CreateElement("div", "section_text", ao);
  Web.AddButton(
    "Back to city shops",
    "/market",
    5,
    Settings.BackToMarket,
    buttonsRoot
  );
};

Settings.Enable = function () {
  var addon = Web.GetLastPartOfHref(this);
  Settings.Addons.insert(addon, 1);
  Web.SwitchVisibility("button_c3", this.parentNode);
};

Settings.Disable = function () {
  var addon = Web.GetLastPartOfHref(this);
  Settings.Addons.remove(addon);
  Web.SwitchVisibility("button_c2", this.parentNode);
};

Settings.UseLS = function () {
  Storage.switchToLS();
  Web.SwitchVisibility("button_c3", this.parentNode);
  document.getElementById("storage").innerHTML = "localStorage";
};

Settings.UseGM = function () {
  Storage.switchToGM();
  Web.SwitchVisibility("button_c3", this.parentNode);
  document.getElementById("storage").innerHTML = "greasemonkeyStorage";
};

Settings.ShowImport = function () {
  var thisPage =
    this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
  var shopsRoot = thisPage.parentNode;
  Settings.CreateImportPage();
  Web.SwitchVisibility("ImportExport", thisPage);
  scroll(0, 0);
};

Settings.CreateImportPage = function () {
  var page = Web.CreatePage(
    Settings.ImportShopName,
    Settings.ImportShopDesc,
    "market",
    "ImportExport",
    "wireframe_2"
  );
  var ie = Web.CreateElement(
    "div",
    "wireframe_market common_content common_wl",
    page
  );
  var ieTitle = Web.CreateElement("div", "paragraph_title c2", ie);
  Web.appendText("Import / Export", ieTitle);

  var exportSelector = Web.CreateElement("div", "paragraph_text", ie);
  Web.appendText("Export: ", exportSelector);
  var select = Web.CreateElement("select", "select", exportSelector);
  select.setAttribute("id", "export-selector");
  Web.CreateElement("option", undefined, select).appendChild(
    document.createTextNode("--Everything--")
  );
  Web.CreateElement("option", undefined, select).appendChild(
    document.createTextNode("--Static data--")
  );
  Web.CreateElement("option", undefined, select).appendChild(
    document.createTextNode("--Personal data--")
  );
  Storage.foreach(function (key) {
    Web.CreateElement("option", undefined, select).appendChild(
      document.createTextNode(key)
    );
  });

  var ieTextArea = Web.CreateElement("textarea", "c2 bd1 textarea", ie);
  ieTextArea.setAttribute("cols", "90");
  ieTextArea.setAttribute("rows", "10");

  var ieButtonsRoot = Web.CreateElement("div", "section_text", ie);
  Web.AddButton(
    "Import",
    "/market/import",
    5,
    Settings.Import,
    ieButtonsRoot,
    true
  );
  Web.AddButton(
    "Export",
    "/market/export",
    5,
    Settings.Export,
    ieButtonsRoot,
    true
  );
  Web.AddButton("Wipe", "/market/wipe", 5, Settings.Wipe, ieButtonsRoot, true);

  var buttonsRoot = Web.CreateElement("div", "section_text", ie);
  Web.AddButton(
    "Back to city shops",
    "/market",
    5,
    Settings.BackToMarket,
    buttonsRoot
  );
};

Settings.Export = function () {
  var container = new Map(undefined, ",,,", ";;;");
  var select = document.getElementById("export-selector");
  switch (select.value) {
    case "--Everything--":
      Settings.ExportVariables(container, function () {
        return true;
      });
      break;
    case "--Static data--":
      Settings.ExportVariables(container, Settings.IsStaticVariableName);
      break;
    case "--Personal data--":
      Settings.ExportVariables(container, function (name) {
        return !Settings.IsStaticVariableName(name);
      });
      break;
    default:
      container.insert(select.value, Storage.get(select.value));
  }
  container.insert("DataVersion", Storage.get("DataVersion"));
  document.getElementsByTagName("textarea")[0].value = container.toString();
};

Settings.ExportVariables = function (container, include) {
  Storage.foreach(function (key) {
    console.log(key + ":" + include(key));
    if (include(key)) container.insert(key, Storage.get(key));
  });
};

Settings.IsStaticVariableName = function (name) {
  return (
    name == "ShopNames" ||
    name == "Items" ||
    name == "CraftShops" ||
    name == "ItemShops" ||
    name.indexOf("Shop_") == 0
  );
};

Settings.Wipe = function () {
  var keys = Storage.list();
  for (var key = 0; key < keys.length; key++) {
    var value = keys[key];
    Storage.remove(value);
  }
};

Settings.Import = function () {
  var container = new Map(
    document.getElementsByTagName("textarea")[0].value,
    ",,,",
    ";;;"
  );
  var containerVersion = container.get("DataVersion")[0];
  if (containerVersion == undefined) {
    if (
      !confirm(
        "You are trying to import data from an unknown version. Are you sure you do want to continue?"
      )
    )
      return;
    containerVersion = "110301"; // assume it is from the version just before implementing this
  }
  if (containerVersion < dataVersion) {
    // TODO: update data first
    // - do backup, clear, import, update, export, import backup, import
    // OR run backup on container - another implementation of Storage

    if (
      !confirm(
        "You are trying to import data from version " +
          containerVersion +
          " but you have version " +
          dataVersion +
          ". There probably were some updates in data structure in the meantime. Do you want to try your luck and import it anyway?"
      )
    )
      return;
    container.remove("DataVersion");
  }
  if (containerVersion > dataVersion) {
    alert(
      "Imported data are from newer version of this script. Please update it first."
    );
    return;
  }

  container.foreach(function (key, values) {
    Storage.set(key, values[0]);
  });
};

Settings.Attach = function () {
  if (Launcher.UrlTest.EndsWith("/market")(document.URL))
    Settings.AlterMarket(document);
};

///////////////////////
// Item availability //
///////////////////////

ItemAvailability = {};

ItemAvailability.Items = function () {
  this.gmmap = new GMMap("Items", false, false);
};
ItemAvailability.Items.prototype.add = function (itemId, shopId) {
  return this.gmmap.add(itemId, shopId);
};
ItemAvailability.Items.prototype.getShops = function (itemId) {
  return this.gmmap.get(itemId);
};
ItemAvailability.Items.prototype.save = function () {
  this.gmmap.save();
};

ItemAvailability.ItemsInstance = null;
ItemAvailability.getItems = function () {
  if (ItemAvailability.ItemsInstance == null) {
    ItemAvailability.ItemsInstance = new ItemAvailability.Items();
  }
  return ItemAvailability.ItemsInstance;
};

ItemAvailability.ItemShops = function () {
  this.gmmap = new GMMap("ItemShops", false, false);
};
ItemAvailability.ItemShops.prototype.add = function (shopId, cityId) {
  return this.gmmap.add(shopId, cityId);
};
ItemAvailability.ItemShops.prototype.getCitiesWithShop = function (shopId) {
  return this.gmmap.get(shopId);
};
ItemAvailability.ItemShops.prototype.save = function () {
  this.gmmap.save();
};

ItemAvailability.ItemShopsInstance = null;
ItemAvailability.getItemShops = function () {
  if (ItemAvailability.ItemShopsInstance == null) {
    ItemAvailability.ItemShopsInstance = new ItemAvailability.ItemShops();
  }
  return ItemAvailability.ItemShopsInstance;
};

ItemAvailability.IsItemShop = function (shopType) {
  return shopType[0] == "1" || shopType[0] == "2";
};

ItemAvailability.GetShopIdFromName = function (name) {
  return name.substring(5);
};

ItemAvailability.GetCitiesSellingItem = function (itemId) {
  this.cities = new Array();
  this.shopInThisCity = null;
  var items = ItemAvailability.getItems();
  var shops = items.getShops(itemId);
  for (var shop = 0; shop < shops.length; shop++) {
    var shopId = shops[shop];

    var cityIds = ItemAvailability.getItemShops().getCitiesWithShop(shopId);
    for (var cKey = 0; cKey < cityIds.length; cKey++) {
      var cityId = cityIds[cKey];
      if (this.cities.indexOf(cityId) == -1) this.cities.push(cityId); // avoid duplicities
      if (cityId == Estiah.CurrentCity) {
        this.shopInThisCity = shopId;
      }
    }
  }
  //GM_log(itemId + ' is in ' + this.cities.length + ' cities: ' + this.cities.join(','));
  return this;
};

ItemAvailability.LoadItemShop = function (doc, shopId) {
  var itemsList = ItemAvailability.getItems();
  var items = doc.getElementsByClassName("itreasure BV_system_file"); // grey excluded
  for (var i = 0; i < items.length; i++) {
    var itemId = Estiah.GetItemId(items[i]);
    //GM_log(itemId);
    itemsList.add(itemId, shopId);
  }
  itemsList.save();
  var itemShops = ItemAvailability.getItemShops();
  itemShops.add(shopId, Estiah.CurrentCity);
  itemShops.save();
};

ItemAvailability.ShowCities = function () {
  Web.SwitchVisibility("cities", this.parentNode);
};

ItemAvailability.ShowBuy = function () {
  Web.SwitchVisibility("buy", this);
};

ItemAvailability.AlterCraftShops = function (root) {
  var shops = root.getElementsByClassName(
    "wireframe_craft common_content common_wl"
  );
  if (shops.lenght == 0) ItemAvailability.AlterCraftShop(root);
  else
    for (var i = 0; i < shops.length; i++) {
      var shopCities = shops[i].getElementsByClassName("shop-city");
      var shopCity =
        shopCities.length > 0 ? shopCities[0].innerHTML : undefined;
      ItemAvailability.AlterCraftShop(shops[i], shopCity);
    }
};

ItemAvailability.AlterCraftShop = function (root, shopCity) {
  var items = root.getElementsByClassName("nolink itreasure BV_system_file"); // removed 'mat '
  for (var i = 0; i < items.length; i++) {
    var itemId = Estiah.GetItemId(items[i]);
    var itemInfo = ItemAvailability.GetCitiesSellingItem(itemId);
    var cities = itemInfo.cities;
    if (cities.length > 0) {
      var insertPlaceholder = document.createElement("span");
      items[i].parentNode.insertBefore(
        insertPlaceholder,
        Web.FindBrAfter(items[i])
      );
      Web.appendText(" ", insertPlaceholder);

      // show
      var citiesSpan = document.createElement("span");
      citiesSpan.setAttribute("class", "cities");

      var outerspan;
      for (var cityIt = 0; cityIt < cities.length; cityIt++) {
        if (cityIt % 2 == 0) {
          outerSpan = document.createElement("span");
          outerSpan.style.display = "inline-block";
          outerSpan.style.padding = "1px";
          outerSpan.style.verticalAlign = "middle";
          outerSpan.style.fontSize = "6pt";
          citiesSpan.appendChild(outerSpan);
        } else outerSpan.appendChild(document.createElement("br"));
        var innerSpan = document.createElement("span");
        if (cities[cityIt] == Estiah.CurrentCity)
          innerSpan.style.color = "#F6BA68";
        Web.appendText(cities[cityIt], innerSpan);
        outerSpan.appendChild(innerSpan);
      }

      insertPlaceholder.appendChild(citiesSpan);
      if (itemInfo.shopInThisCity != null) {
        // [Buy]
        var buySpan = document.createElement("span");
        buySpan.setAttribute("class", "buy");
        var a = document.createElement("a");
        a.style.color = "#F6BA68";
        a.setAttribute("href", "/shop/" + itemInfo.shopInThisCity);
        Web.appendText("[Buy]", a);
        buySpan.appendChild(a);
        // alternate
        var alt = document.createElement("span");
        Web.appendText(" [A]", alt);
        buySpan.appendChild(alt);
        alt.addEventListener("click", ItemAvailability.ShowCities, true);

        insertPlaceholder.appendChild(buySpan);

        citiesSpan.setAttribute("style", "display:none");
        citiesSpan.addEventListener("click", ItemAvailability.ShowBuy, true);
      }

      // hide ingredients from crafting place
      if (cities.indexOf(shopCity) != -1) {
        var span = document.createElement("span");
        var element = items[i];
        var br = Web.FindBrAfter(element);
        var parent = element.parentNode;
        while (element != br) {
          var next = element.nextSibling;
          span.appendChild(element);
          element = next;
        }
        span.style.opacity = 0.5;
        parent.insertBefore(span, br);
      }
    }
  }
};

ItemAvailability.AlterInventory = function (doc) {
  var items = doc.getElementsByClassName("common_file floating opacity bd1");
  for (var i = 0; i < items.length; i++) {
    var itemId = items[i].id.substr(14);
    var cities = ItemAvailability.GetCitiesSellingItem(itemId).cities;
    if (cities.length == 0) continue;
    var desc = items[i].getElementsByClassName("description")[0];
    desc.appendChild(document.createElement("br"));
    desc.appendChild(document.createElement("br"));
    Web.appendText("Can be bought in: ", desc);
    Web.appendText(cities.join(", "), Web.CreateElement("span", "c2", desc));
  }
};

Launcher.Register(
  "ItemAvailability",
  "Shows in which cities you can buy an item."
)
  .Read(Estiah.IsItemShopUrl, function (doc, url) {
    ItemAvailability.LoadItemShop(doc, Estiah.GetShopIdFromUrl(url));
  })
  .Modify(Estiah.IsCraftShopUrl, ItemAvailability.AlterCraftShops)
  .Modify(Launcher.UrlTest.EndsWith("/guild"), ItemAvailability.AlterCraftShop)
  .Modify(Estiah.Inventory.UrlTest, ItemAvailability.AlterInventory);

/////////////////
// CraftHelper //
/////////////////

CraftHelper = {};

// CharmShop //
CraftHelper.CharmShop = function (shopId, clear) {
  this.gmmap = new GMMap("Shop_" + shopId, false /*, clear*/);
};
CraftHelper.CharmShop.prototype.addCharm = function (charmId, html) {
  return this.gmmap.insert(charmId, html);
};
CraftHelper.CharmShop.prototype.save = function () {
  this.gmmap.save();
};
CraftHelper.CharmShop.prototype.foreach = function (callback) {
  this.gmmap.foreach(callback);
};
CraftHelper.CharmShop.prototype.getCharm = function (charmId) {
  return this.gmmap.get(charmId)[0];
};

// CharmTracker //
CraftHelper.CharmTracker = function () {
  this.gmmap = new GMMap("CharmTracker_" + Estiah.Player.Id, true);
};
CraftHelper.CharmTracker.prototype.contains = function (shopId, charmId) {
  return this.gmmap.contains(shopId, charmId);
};
CraftHelper.CharmTracker.prototype.track = function (shopId, charmId) {
  return this.gmmap.add(shopId, charmId);
};
CraftHelper.CharmTracker.prototype.untrack = function (shopId, charmId) {
  return this.gmmap.remove(shopId, charmId);
};
CraftHelper.CharmTracker.prototype.foreach = function (callback) {
  return this.gmmap.foreach(callback);
};

CraftHelper.CharmTrackerInstance = null;
CraftHelper.getCharmTracker = function () {
  if (CraftHelper.CharmTrackerInstance == null)
    CraftHelper.CharmTrackerInstance = new CraftHelper.CharmTracker();
  return CraftHelper.CharmTrackerInstance;
};

// CraftShops //
CraftHelper.CraftShops = function () {
  this.gmmap = new GMMap("CraftShops", true);
};
CraftHelper.CraftShops.prototype.addCityShop = function (cityId, shopId) {
  return this.gmmap.add(shopId, cityId);
};
CraftHelper.CraftShops.prototype.getCitiesWithShop = function (shopId) {
  return this.gmmap.get(shopId);
};

CraftHelper.CraftShopsInstance = null;
CraftHelper.getCraftShops = function () {
  if (CraftHelper.CraftShopsInstance == null) {
    CraftHelper.CraftShopsInstance = new CraftHelper.CraftShops();
  }
  return CraftHelper.CraftShopsInstance;
};

// ShopNames //
CraftHelper.ShopNames = function () {
  this.gmmap = new GMMap("ShopNames", true);
};
CraftHelper.ShopNames.prototype.addShop = function (shopId, name, desc) {
  return this.gmmap.insert(shopId, name, desc);
};
CraftHelper.ShopNames.prototype.foreach = function (callback) {
  this.gmmap.foreach(callback);
};
CraftHelper.ShopNames.prototype.getShopName = function (shopId) {
  return this.gmmap.get(shopId);
};
CraftHelper.ShopNamesInstance = null;
CraftHelper.GetShopNames = function () {
  if (CraftHelper.ShopNamesInstance == null)
    CraftHelper.ShopNamesInstance = new CraftHelper.ShopNames();
  return CraftHelper.ShopNamesInstance;
};

CraftHelper.UpdateCharmAndItemCounts = function (root, doneCallback) {
  Estiah.Fetcher.FetchItemAndCharmCount(function (itemCounts, charmCounts) {
    var counts = root.getElementsByClassName("count");
    for (var c = 0; c < counts.length; c++) {
      var count = charmCounts[Estiah.Charms._getCharmIdFromCountEl(counts[c])];
      counts[c].innerHTML = count == undefined ? "0" : count;
    }

    var items = root.getElementsByClassName("mat nolink BV_system_file");
    for (var key = 0; key < items.length; key++) {
      var item = items[key];
      var itemId = item.href.substring(item.href.lastIndexOf("/") + 1);

      var count = itemCounts[itemId];

      var toUpdate = item.parentNode.getElementsByClassName(
        "PT_update_count_t0i" + itemId
      );
      for (var uKey = 0; uKey < toUpdate.length; uKey++) {
        toUpdate[uKey].innerHTML = count == undefined ? "0" : count;
      }
    }
    if (doneCallback != undefined) doneCallback();
  });
};

CraftHelper.LoadVendorShop = function (doc, shopId) {
  // 3
  CraftHelper.LoadCharmShop(doc, shopId, "cardframe_lr", 9);
};

CraftHelper.LoadCraftShop = function (doc, shopId) {
  // 4, 6
  CraftHelper.LoadCharmShop(doc, shopId, "craft", 5);
};

CraftHelper.LoadCharmShop = function (doc, shopId, className, charmIdOffset) {
  CraftHelper.getCraftShops().addCityShop(Estiah.CurrentCity, shopId);
  var charmMap = new CraftHelper.CharmShop(shopId, true);
  var charms = doc.getElementsByClassName(className);
  for (var i = 0; i < charms.length; i++) {
    var charmId = charms[i].id.substring(charmIdOffset);
    charmMap.addCharm(charmId, charms[i].innerHTML);
  }
  charmMap.save();
};

CraftHelper.AlterMarket = function (doc) {
  var backButton = doc.getElementsByClassName("button button_c5")[0];
  backButton.setAttribute(
    "class",
    backButton.getAttribute("class") + " decision"
  );
  Web.AddButton(
    "Show all shops",
    "/market/all",
    5,
    CraftHelper.ShowAllShops,
    backButton.parentNode,
    true
  );
};

CraftHelper.AlterCraftShop = function (doc, url) {
  var shopId = Estiah.GetShopIdFromUrl(url);
  //GM_log(shopId);
  if (shopId == "tracked") return;
  var charms = doc.getElementsByClassName("craft");
  for (var i = 0; i < charms.length; i++) {
    CraftHelper.AddTrackButton(charms[i], shopId);
  }
};

CraftHelper.CreateAllShopsPage = function () {
  var allShops = Web.CreatePage(
    "All shops",
    "Shops from all visited cities",
    "market",
    "AllShops",
    "wireframe_2"
  );

  var shopsFrame = Web.CreateElement(
    "div",
    "wireframe_market common_content common_wl",
    allShops
  );
  var shopRoot = Web.CreateElement("div", "section_text format", shopsFrame);

  var tracking = Estiah.Market.AddShopGroup(
    "bread",
    "Interesting Stuff",
    shopRoot
  );
  CraftHelper.AddTrackingShop(tracking);

  var vendor = null;
  var craft = null;
  var pvp = null;
  var event = null;
  CraftHelper.GetShopNames().foreach(function (shopId, info) {
    switch (shopId[0]) {
      case "3":
        if (vendor == null)
          vendor = Estiah.Market.AddShopGroup(
            "staff",
            "Vendor Shops",
            shopRoot
          );
        CraftHelper.AddShop(shopId, "cvendor", info[0], info[1], vendor);
        break;
      case "4":
        if (craft == null)
          craft = Estiah.Market.AddShopGroup("anvil", "Craft Shops", shopRoot);
        CraftHelper.AddShop(shopId, "ccraft", info[0], info[1], craft);
        break;
      case "5":
        if (event == null)
          event = Estiah.Market.AddShopGroup("axe", "Event Shops", shopRoot);
        CraftHelper.AddShop(shopId, "item", info[0], "", event);
        break;
      case "6":
        if (pvp == null)
          pvp = Estiah.Market.AddShopGroup("crosssword", "Pvp Shops", shopRoot);
        CraftHelper.AddShop(shopId, "ipvp", info[0], info[1], pvp);
        break;
      default:
        break;
    }
  });

  var buttonsRoot = Web.CreateElement("div", "section_text", shopsFrame);
  Web.AddButton(
    "Back to city shops",
    "/market",
    5,
    CraftHelper.ShowCityShops,
    buttonsRoot
  );
};

CraftHelper.AddShop = function (shopId, className, shopName, shopDesc, root) {
  var shop = Estiah.Market.AddShop(
    shopId,
    className,
    shopName,
    shopDesc,
    CraftHelper.ShowShop,
    root
  );

  var cities = CraftHelper.getCraftShops().getCitiesWithShop(shopId);
  var desc = shop.getElementsByClassName("description");
  for (var i = 0; i < desc.length; i++) {
    var citiesEl = Web.CreateElement("span", "dex", desc[i]);
    Web.appendText(" (" + cities.join(", ") + ")", citiesEl);
  }
};

CraftHelper.AddTrackingShop = function (root) {
  Estiah.Market.AddShop(
    "tracked",
    "item",
    "Tracked Charms",
    "Charms you have had marked for tracking",
    CraftHelper.CreateAndShowTracked,
    root
  );
};

CraftHelper.ShowAllShops = function () {
  var thisPage = this.parentNode.parentNode.parentNode.parentNode;
  var shopsRoot = thisPage.parentNode;
  if (shopsRoot.getElementsByClassName("AllShops").length == 0)
    CraftHelper.CreateAllShopsPage();
  Web.SwitchVisibility("AllShops", thisPage);
  scroll(0, 0);
};

CraftHelper.ShowCityShops = function () {
  Web.SwitchVisibility(
    "CityShops",
    this.parentNode.parentNode.parentNode.parentNode
  );
  scroll(0, 0);
};

CraftHelper.ShowShop = function () {
  CraftHelper.ScrollTop = document.documentElement.scrollTop;
  var shopId = this.href.substring(this.href.lastIndexOf("/") + 1);
  var cityShops =
    this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
  var info = CraftHelper.GetShopNames().getShopName(shopId);
  var shop;
  switch (shopId[0]) {
    case "3":
      shop = CraftHelper.CreateVendorShopPage(shopId, info[0], info[1]);
      break;
    case "4":
    case "5":
    case "6":
      shop = CraftHelper.CreateCraftShopPage(shopId, info[0], info[1]);
      break;
    default:
      return;
  }
  CraftHelper.PostprocessShop(shopId, shop);
  Web.SwitchVisibility("Shop" + shopId, cityShops);
};

CraftHelper.PostprocessShop = function (shopId, shop) {
  CraftHelper.UpdateCharmAndItemCounts(shop, function () {
    scroll(0, 0);
    Launcher.NewPageCreated("/shop/" + shopId, shop);
  });
};

CraftHelper.BackFromShop = function () {
  var thisPage = this.parentNode.parentNode.parentNode.parentNode.parentNode;
  Web.SwitchVisibility("AllShops", thisPage);
  // delete it
  thisPage.parentNode.removeChild(thisPage);
  scroll(0, CraftHelper.ScrollTop);
};

CraftHelper.CreateAndShowTracked = function () {
  CraftHelper.ScrollTop = document.documentElement.scrollTop;
  var allShops =
    this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;

  var page = Web.CreatePage(
    "Tracked Charms",
    "Charms you have had marked for tracking",
    "craft",
    "tracked",
    "wireframe_2"
  );
  page.id = "trackingShop";
  var crafts = null;
  CraftHelper.getCharmTracker().foreach(function (shopId, charmIds) {
    var shop = new CraftHelper.CharmShop(shopId);

    shopDesc = CraftHelper.GetShopNames().getShopName(shopId);
    var title = Web.CreatePageTitle(
      shopDesc[0] + CraftHelper.GetCitiesWithShopHtml(shopId),
      shopDesc[1],
      "craft",
      page
    );
    crafts = Web.CreateElement("div", "craftlist format log", title);
    var craftSection;
    var i = 0;
    for (var key = 0; key < charmIds.length; key++) {
      var charmId = charmIds[key];
      if (i++ % 3 == 0)
        craftSection = Web.CreateElement("div", "section_text", crafts);
      var div = Web.CreateElement("div", "craft", craftSection);
      div.id = "Craft" + charmId;
      div.innerHTML = shop.getCharm(charmId);
      CraftHelper.ClearCraftFunctions(div);
      CraftHelper.AddTrackButton(div, shopId);
    }
  });
  if (crafts == null) {
    var title = Web.CreatePageTitle("No tracked charms yet", "", "craft", page);
    crafts = Web.CreateElement("div", "craftlist format log", title);
  }
  var buttonsRoot = Web.CreateElement("div", "section_text", crafts);
  Web.AddButton(
    "Back to market",
    "/market/all",
    5,
    CraftHelper.BackFromShop,
    buttonsRoot
  );

  CraftHelper.PostprocessShop("tracked", page);
  Web.SwitchVisibility("tracked", allShops);
};

CraftHelper.GetCitiesWithShopHtml = function (shopId) {
  var cities = CraftHelper.getCraftShops().getCitiesWithShop(shopId);
  var html = " (";
  for (var i = 0; i < cities.length; i++) {
    // note: interaction with ItemAvailability
    if (html.length > 5) html += ", ";
    html += '<span class="shop-city c2">' + cities[i] + "</span>";
  }
  return html + ")";
};

CraftHelper.CreateCraftShopPage = function (shopId, shopName, shopDesc) {
  //GM_log(shopId);
  var shop = new CraftHelper.CharmShop(shopId);
  var page = Web.CreatePage(
    shopName + CraftHelper.GetCitiesWithShopHtml(shopId),
    shopDesc,
    "craft",
    "Shop" + shopId,
    "wireframe_2"
  );
  var crafts = Web.CreateElement(
    "div",
    "craftlist format log",
    page.firstChild
  );
  var craftSection;
  var i = 0;
  shop.foreach(function (charmId, charmHTML) {
    if (charmHTML == undefined) return;
    if (i++ % 3 == 0)
      craftSection = Web.CreateElement("div", "section_text", crafts);
    var div = Web.CreateElement("div", "craft", craftSection);
    div.id = "Craft" + charmId;
    div.innerHTML = charmHTML;
    CraftHelper.ClearCraftFunctions(div);
  });
  var buttonsRoot = Web.CreateElement("div", "section_text", crafts);
  Web.AddButton(
    "Back to market",
    "/market/all",
    5,
    CraftHelper.BackFromShop,
    buttonsRoot
  );
  return page;
};

CraftHelper.CreateVendorShopPage = function (shopId, shopName, shopDesc) {
  //GM_log(shopId);
  var shop = new CraftHelper.CharmShop(shopId);
  var page = Web.CreatePage(
    shopName + CraftHelper.GetCitiesWithShopHtml(shopId),
    shopDesc,
    "shop",
    "Shop" + shopId,
    "wireframe_2"
  );
  var items = Web.CreateElement(
    "div",
    "inventory shop format log",
    page.firstChild
  );
  var goodsSection = Web.CreateElement("div", "goodslist c2", items);
  var i = 0;
  shop.foreach(function (charmId, charmHTML) {
    var div = Web.CreateElement(
      "div",
      "cardframe_lr cardbg_lr_common",
      goodsSection
    );
    div.id = "ShopGoods" + charmId;
    div.innerHTML = charmHTML;
    var buys = div.getElementsByClassName("BV_shop_buy");
    for (var buy = 0; buy < buys.length; buy++) Web.removeNode(buys[buy]);
  });
  var buttonsRoot = Web.CreateElement("div", "section_text format", items);
  Web.AddButton(
    "Back to market",
    "/market/all",
    5,
    CraftHelper.BackFromShop,
    buttonsRoot
  );
  return page;
};

CraftHelper.ClearCraftFunctions = function (craft) {
  var functions = craft.getElementsByClassName("functions");
  for (var buttons = 0; buttons < functions.length; buttons++)
    Web.removeNode(functions[buttons]);
};

CraftHelper.AddTrackButton = function (craft, shopId) {
  var functions = craft.getElementsByClassName("functions");
  var functs = null;
  if (functions.length > 0) functs = functions[0];
  else {
    var contents = craft.getElementsByClassName("content");
    if (contents.length == 0) return;
    functs = Web.CreateElement("div", "functions");
    Web.AddAsFirstChild(functs, contents[0]);
  }
  var craftId = craft.id.substring(5);
  var href = shopId + "/" + craftId;
  var track = Web.AddInnerLink(
    "[Track]",
    href,
    "nolink track",
    CraftHelper.TrackCharm,
    functs
  );
  var untrack = Web.AddInnerLink(
    "[Untrack]",
    href,
    "nolink untrack",
    CraftHelper.UntrackCharm,
    functs
  );
  if (CraftHelper.getCharmTracker().contains(shopId, craftId))
    track.style.display = "none";
  else untrack.style.display = "none";
};

CraftHelper.TrackCharm = function () {
  var ids = this.href.split("/");
  CraftHelper.getCharmTracker().track(ids[ids.length - 2], ids[ids.length - 1]);
  Web.SwitchVisibility("untrack", this);
};

CraftHelper.UntrackCharm = function () {
  var ids = this.href.split("/");
  CraftHelper.getCharmTracker().untrack(
    ids[ids.length - 2],
    ids[ids.length - 1]
  );
  Web.SwitchVisibility("track", this);
};

CraftHelper.ReadShopNamesFromCity = function (doc) {
  var eventShops = doc.evaluate(
    '//div[@class="site"]//div[@class="functions"]/a[. = "[Shop]"]',
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < eventShops.snapshotLength; i++) {
    var shopId = Estiah.GetShopIdFromUrl(eventShops.snapshotItem(i).href);
    var names = eventShops
      .snapshotItem(i)
      .parentNode.parentNode.getElementsByClassName("lhp");
    for (var name = 0; name < names.length; name++)
      CraftHelper.GetShopNames().addShop(shopId, names[name].innerHTML);
  }
};

CraftHelper.ReadShopNamesFromMarket = function (doc) {
  var Shops = doc.evaluate(
    "//*[@class='stand']/*[@class='name bd1']/*", // shops
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < Shops.snapshotLength; i++) {
    var shopId = Estiah.GetShopIdFromUrl(String(Shops.snapshotItem(i)));
    // no item shops
    if (shopId[0] == "1" || shopId[0] == "2") continue;
    var descriptions =
      Shops.snapshotItem(i).parentNode.parentNode.getElementsByClassName(
        "description"
      );
    for (var desc = 0; desc < descriptions.length; desc++)
      CraftHelper.GetShopNames().addShop(
        shopId,
        Shops.snapshotItem(i).innerHTML,
        descriptions[desc].innerHTML
      );
  }
};

CraftHelper.AlterInventory = function (doc) {
  Estiah.Charms.FetchCharmCounts(function (charmCounts) {
    var itemNames = new Object();
    var receipts = new Object();
    var itemUsageCh = new Map();
    var itemUsageI = new Map();

    // parse charm data
    var temp = document.createElement("div");
    CraftHelper.GetShopNames().foreach(function (shopId, info) {
      var shop = new CraftHelper.CharmShop(shopId);
      shop.foreach(function (craftId, craftHTML) {
        temp.innerHTML = craftHTML;
        var title = temp.getElementsByClassName("recipe outline c2");
        if (title.length == 0) return;
        var receipt = new Object();
        receipt.craftName = title[0].innerHTML;
        receipt.shopId = shopId;
        var charmId = Estiah.Charms.GetCharmId(temp);
        if (charmId != undefined) {
          receipt.charmId = charmId;
        } else {
          // item
          var itemLink = temp.querySelector(".list + .list a");
          receipt.itemId = Web.GetLastPartOfHref(itemLink);
          // console.log(shopId + '/' + receipt.itemId + ' ' + receipt.craftName);
        }
        var ingredients = temp.querySelectorAll(".recipe + .list .content > a");
        //GM_log('ingredients of ' + receipt.craftName + ' (' + charmId + ')');
        for (var i = 0; i < ingredients.length; i++) {
          var itemName = ingredients[i].innerHTML;
          var itemId = Web.GetLastPartOfHref(ingredients[i]);
          var itemCount = Web.FindTagAfter(
            ingredients[i],
            "SPAN"
          ).innerHTML.substr(1);

          //GM_log(itemName + ' (' + itemId + ') x ' + itemCount);
          itemNames[itemId] = itemName;
          receipt[itemId] = itemCount;
          if (charmId == undefined) itemUsageI.add(itemId, craftId);
          else itemUsageCh.add(itemId, craftId);
        }
        receipts[craftId] = receipt;
      });
    });

    var inventoryItemCounts = Estiah.Inventory._getItemCounts(doc);

    var items = doc.getElementsByClassName("common_file floating opacity bd1");
    for (var i = 0; i < items.length; i++) {
      var itemId = items[i].id.substr(14);
      var usageCh = itemUsageCh.get(itemId);
      var usageI = itemUsageI.get(itemId);
      if (usageCh.length == 0 && usageI.length == 0) continue;
      var desc = items[i].querySelector(".description");
      desc.appendChild(document.createElement("br"));
      desc.appendChild(document.createElement("br"));
      Web.appendText("Useful in following crafts:", desc);
      if (usageCh.length > 0) {
        for (var j = 0; j < usageCh.length; j++) {
          var craftId = usageCh[j];
          CraftHelper.AddInventoryTooltipItem(
            desc,
            itemId,
            craftId,
            receipts[craftId],
            charmCounts
          );
        }
      }
      if (usageI.length > 0) {
        if (usageCh.length > 0) desc.appendChild(document.createElement("br"));
        for (var j = 0; j < usageI.length; j++) {
          desc.appendChild(document.createElement("br"));
          var craftId = usageI[j];
          var craftName = receipts[craftId].craftName;
          var itemCount = receipts[craftId][itemId];
          var resultItemId = receipts[craftId].itemId;
          var resultItemCount = inventoryItemCounts[resultItemId];
          if (resultItemCount == undefined) resultItemCount = 0;
          var craftDesc = Web.CreateElement("span", "craft", desc);
          Web.appendText("x" + itemCount + " in ", craftDesc);
          var color = CraftHelper.getCharmTracker().contains(
            receipts[craftId].shopId,
            craftId
          )
            ? "iepic"
            : "c2";
          Web.appendText(
            craftName,
            Web.CreateElement("span", color, craftDesc)
          );
          Web.appendText(" (" + resultItemCount + ")", craftDesc);

          var resultInCharms = itemUsageCh.get(resultItemId);
          var needed = true;
          if (resultInCharms.length > 0) {
            needed = false;
            for (var k = 0; k < resultInCharms.length; k++) {
              var chCraftId = resultInCharms[k];
              needed |= CraftHelper.AddInventoryTooltipItem(
                desc,
                resultItemId,
                chCraftId,
                receipts[chCraftId],
                charmCounts,
                "- "
              );
            }
          }
          if (!needed) craftDesc.style.opacity = 0.5;
        }
      }
    }
  });
};

CraftHelper.AddInventoryTooltipItem = function (
  desc,
  itemId,
  craftId,
  receipt,
  charmCounts,
  prefix
) {
  desc.appendChild(document.createElement("br"));
  var charmId = receipt.charmId;
  var charmName = receipt.craftName;
  var itemCount = receipt[itemId];
  var charmCount = charmCounts[charmId];
  if (charmCount == undefined) charmCount = 0;
  var craftDesc = Web.CreateElement("span", "craft", desc);
  if (prefix != undefined) Web.appendText(prefix, craftDesc);
  Web.appendText("x" + itemCount + " in ", craftDesc);
  var color = CraftHelper.getCharmTracker().contains(receipt.shopId, craftId)
    ? "iepic"
    : "c2";
  Web.appendText(charmName, Web.CreateElement("span", color, craftDesc));
  Web.appendText(" (" + charmCount + ")", craftDesc);
  if (charmCount == 5) {
    craftDesc.style.opacity = 0.5;
    return false;
  }
  return true;
};

Launcher.Register(
  "CraftHelper",
  "Remembers all charms you could want to craft. Also allows to track interesting ones."
)
  .Read(Estiah.IsVendorShopUrl, function (doc, url) {
    CraftHelper.LoadVendorShop(doc, Estiah.GetShopIdFromUrl(url));
  })
  .Read(Estiah.IsCraftShopUrl, function (doc, url) {
    CraftHelper.LoadCraftShop(doc, Estiah.GetShopIdFromUrl(url));
  })
  .Modify(Estiah.IsCraftShopUrl, CraftHelper.AlterCraftShop)
  .Read(Launcher.UrlTest.EndsWith("/city"), CraftHelper.ReadShopNamesFromCity)
  .Read(
    Launcher.UrlTest.EndsWith("/market"),
    CraftHelper.ReadShopNamesFromMarket
  )
  .Modify(Launcher.UrlTest.EndsWith("/market"), CraftHelper.AlterMarket)
  .Modify(Estiah.Inventory.UrlTest, CraftHelper.AlterInventory);

/////////////////////////////////////
// Item availability & Marketplace //
/////////////////////////////////////

CityLoader = {};

CityLoader.LoadCity = function (doc) {
  //GM_log('loading city ' + Estiah.CurrentCity);
  // loading shops is connected to web.httprequest function, it will be handled automatically - therefore empty success functions

  var eventShops = doc.evaluate(
    '//div[@class="site"]//div[@class="functions"]/a[. = "[Shop]"]',
    doc,
    null,
    XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
  for (var i = 0; i < eventShops.snapshotLength; i++) {
    Web.HttpRequest(
      eventShops.snapshotItem(i).getAttribute("href"),
      function () {}
    );
  }

  Web.HttpRequest("http://www.estiah.com/market", function (response) {
    var Shops = response.evaluate(
      "//*[@class='stand']/*[@class='name bd1']/*", // shops
      response,
      null,
      XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
      null
    );
    for (var i = 0; i < Shops.snapshotLength; i++) {
      Web.HttpRequest(
        Shops.snapshotItem(i).getAttribute("href"),
        function () {}
      );
    }
  });
};

Launcher.Register(
  "CityLoader",
  "When you enter the city, this loads all city shops for you. Useful with ItemAvailability and CraftHelper."
).Read(Launcher.UrlTest.EndsWith("/city"), CityLoader.LoadCity);

///////////////////
// AutoSelectPet //
///////////////////

AutoSelectPet = {};
AutoSelectPet.ClickByClassName = function (doc, className) {
  var pets = doc.getElementsByClassName(className);
  if (pets.length == 0) return;
  Web.MouseClick(pets[0]);
};
AutoSelectPet.Race = function (doc) {
  AutoSelectPet.ClickByClassName(doc, "BV_race_choose");
};
AutoSelectPet.Fight = function (doc) {
  AutoSelectPet.ClickByClassName(doc, "BV_admission_choose");
};

Launcher.Register(
  "AutoSelectPet",
  "Preselects your first pet for a race / fight."
)
  .Modify(Launcher.UrlTest.EndsWith("/city/race"), AutoSelectPet.Race)
  .Modify(Launcher.UrlTest.EndsWith("/city/fight"), AutoSelectPet.Fight);

///////////////
// NewCharms //
///////////////

NewCharms = {};
NewCharms.SeenCards = function () {
  this.gmlist = new GMList("NewCharms_SeenCards_" + Estiah.Player.Id, true);
};
NewCharms.SeenCards.prototype.Add = function (cardId) {
  return this.gmlist.add(cardId);
};
NewCharms.SeenCards.prototype.Contains = function (cardId) {
  return this.gmlist.contains(cardId);
};
NewCharms.SeenCards.prototype.IsEmpty = function () {
  return this.gmlist.count() == 0;
};

NewCharms.SeenCardsInstance = null;

NewCharms.AlterDeckPage = function (doc) {
  NewCharms.SeenCardsInstance = new NewCharms.SeenCards();
  var cards = doc.getElementsByClassName("card PT_picker_all");
  var firstRun = NewCharms.SeenCardsInstance.IsEmpty();
  var placeholder = null;
  for (var i = 0; i < cards.length; i++) {
    var card = cards[i];
    var cardId = card.id.substr("CollectionCard".length);
    if (firstRun) {
      NewCharms.SeenCardsInstance.Add(cardId);
      continue;
    }
    if (!NewCharms.SeenCardsInstance.Contains(cardId)) {
      if (placeholder == null) {
        placeholder = document.createElement("div");
        placeholder.setAttribute("class", "cardlist active");
        Web.AddAsFirstChild(
          placeholder,
          doc.getElementsByClassName("z2 format")[0]
        );

        Web.appendText(
          "New Charms",
          Web.CreateElement("div", "title", placeholder)
        );

        var info = Web.CreateElement("div", "info highlight", placeholder);
        Web.appendText(
          "You can use new charms!",
          Web.CreateElement("div", "total", info)
        );
        var funcs = Web.CreateElement("div", "functions", info);
        Web.AddInnerLink(
          "[Hide all]",
          "card/hide/all",
          "",
          NewCharms.HideAllEvent,
          funcs
        );
      }
      var cardDiv = Web.CreateElement("div", "card", placeholder);
      var plusA = card.getElementsByClassName("BV_deck_add")[0];
      Web.AddInnerLink(
        "[+]",
        "/card/" + cardId,
        "func_tier",
        Web.CreateMouseClickFunction(plusA),
        cardDiv
      );
      var nameDiv = Web.CreateElement("div", "name", cardDiv);
      var nameA = card.getElementsByClassName("BV_system_file")[0];
      var cloneA = nameA.cloneNode(true);
      cloneA.removeAttribute("id");
      nameDiv.appendChild(cloneA);
      var funcs = Web.CreateElement("div", "functions", cardDiv);
      Web.AddInnerLink(
        "[Hide]",
        "card/hide/" + cardId,
        "func_sec lhp",
        NewCharms.HideEvent,
        funcs
      );
    }
  }
  // connect events (tooltips with charm descriptions)
  if (unsafeWindow.System != undefined)
    unsafeWindow.System.register(unsafeWindow.System.rules);
};

NewCharms.HideAllEvent = function () {
  var cardlist = this.parentNode.parentNode.parentNode;
  var cards = cardlist.getElementsByClassName("func_sec lhp");
  for (var i = 0; i < cards.length; i++) {
    var cardId = Web.GetLastPartOfHref(cards[i]);
    NewCharms.SeenCardsInstance.Add(cardId);
  }
  cardlist.style.display = "none";
};

NewCharms.HideEvent = function () {
  var cardId = Web.GetLastPartOfHref(this);
  NewCharms.SeenCardsInstance.Add(cardId);
  this.parentNode.parentNode.style.display = "none";
};

Launcher.Register(
  "NewCharms",
  "Point out that there are new charms you can use."
).Modify(Launcher.UrlTest.Contains("/character/deck"), NewCharms.AlterDeckPage);

///////////////////////
// Penetrated damage //
///////////////////////

String.getTextBetween = function (str, startText, endText) {
  if (str == undefined) return undefined;
  var start = str.indexOf(startText);
  var end = str.indexOf(endText, start);
  if (start < 0 || end < 0) return undefined;
  start += startText.length;
  return str.substring(start, end);
};

PenetratedDmg = {};
PenetratedDmg.PenetratedDmg = function (root, type) {
  var descs = root.getElementsByClassName(type);
  var penetratedDmg = 0;
  for (var i = 0; i < descs.length; i++) {
    var str = descs[i].innerHTML;
    var times = 1;
    var dmg = String.getTextBetween(str, "<strong>", "</strong>");
    if (dmg == undefined) {
      dmg = String.getTextBetween(str, "", " ");
      if (descs[i].nextSibling != null) {
        var textAfter = descs[i].nextSibling.nodeValue;
        var timesText = String.getTextBetween(textAfter, "during ", " turns");
        if (timesText != undefined) times = parseInt(timesText);
      }
    }
    var penetration = String.getTextBetween(str, "(", "%");
    if (dmg == undefined || penetration == undefined) continue;
    penetratedDmg +=
      Math.floor((parseInt(dmg) * parseInt(penetration)) / 100) * times;
    //descs[i].innerHTML = str.replace(')', ' = ' + penetratedDmg + ')');
  }
  if (penetratedDmg > 0) {
    Web.CreateElement("br", undefined, root);
    Web.CreateElement("br", undefined, root);
    Web.appendText(
      "Penetrated " + type + " " + penetratedDmg,
      Web.CreateElement("span", type, root)
    );
  }
};
PenetratedDmg.AlterDeckPage = function (doc) {
  var descriptions = doc.getElementsByClassName("description");
  for (var i = 0; i < descriptions.length; i++) {
    var desc = descriptions[i];
    PenetratedDmg.PenetratedDmg(desc, "melee");
    PenetratedDmg.PenetratedDmg(desc, "magic");
    PenetratedDmg.PenetratedDmg(desc, "special");
  }
};

Launcher.Register(
  "PenetratedDmg",
  "Did you ever wondered how many damage will this charm do to heavely armored opponent?"
).Modify(
  Launcher.UrlTest.Contains("/character/deck"),
  PenetratedDmg.AlterDeckPage
);

////////////////////////
// Hide used receipts //
////////////////////////

HideUsedReceipts = {};

HideUsedReceipts.AlterShopPage = function (root) {
  var crafts = root.getElementsByClassName("craft");
  for (var c = 0; c < crafts.length; c++) {
    var craft = crafts[c];
    var counts = craft.getElementsByClassName("count");
    if (counts.length == 1 && counts[0].innerHTML == 5)
      craft.style.opacity = 0.3;
  }
};

Launcher.Register(
  "HideUsedReceipts",
  "Hides receipts of charms you have already five of."
)
  .Modify(Estiah.IsCraftShopUrl, HideUsedReceipts.AlterShopPage)
  .Modify(
    Launcher.UrlTest.EndsWith("guild/showcase"),
    HideUsedReceipts.AlterShopPage
  );

/////////////////////////////
// Hide low level bounties //
/////////////////////////////

HideLowLevelBounties = {};
HideLowLevelBounties.TypeSelect = null;

HideLowLevelBounties.AlterPvPPage = function (doc) {
  HideLowLevelBounties.TypeSelect = document.getElementsByName("type")[0];
  var moblist = doc.getElementsByClassName("moblist")[0];
  var players = moblist.getElementsByClassName("mob");
  for (var p = 0; p < players.length; p++)
    HideLowLevelBounties.AlterPlayer(players[p]);
  moblist.addEventListener(
    "DOMNodeInserted",
    HideLowLevelBounties.SomethingInserted,
    false
  );
};

HideLowLevelBounties.SomethingInserted = function (event) {
  var cl = event.target.getAttribute("class");
  if (cl == undefined || cl.indexOf("mob") == -1) return;
  HideLowLevelBounties.AlterPlayer(event.target);
};

HideLowLevelBounties.AlterPlayer = function (player) {
  if (HideLowLevelBounties.TypeSelect.value != "bounty") return;
  var info = document.evaluate(
    'div[contains(@class, "info")]/strong',
    player,
    null,
    XPathResult.FIRST_ORDERED_NODE_TYPE,
    null
  ).singleNodeValue;
  if (info == null) return;
  var level = info.innerHTML;

  if (Estiah.Player.Level - level > 1) player.style.opacity = 0.3;
};

Launcher.Register(
  "HideLowLevelBounties",
  "Hides players among players with bounty with lower level than yours -1."
).Modify(Launcher.UrlTest.EndsWith("pvp"), HideLowLevelBounties.AlterPvPPage);

///////////////////
// Items counter //
///////////////////

ItemsCounter = {};

ItemsCounter.AlterInventoryPage = function (doc) {
  var counts = Estiah.Inventory._getItemCounts(doc);
  for (var itemId in counts) {
    var tooltip = doc.getElementById("SystemInfoItem" + itemId);
    var title = tooltip.getElementsByClassName("title")[0];
    Web.appendText(" (x" + counts[itemId] + ")", title);
  }
};

Launcher.Register(
  "ItemsCounter",
  "Shows sum of inventory and storage item counts in inventory tooltip."
).Modify(Estiah.Inventory.UrlTest, ItemsCounter.AlterInventoryPage);

//////////////////////////
// Storage item watcher //
//////////////////////////

StorageItemsWatcher = {};

StorageItemsWatcher.getItems = function () {
  return new GMMap("ExpiringItems_" + Estiah.Player.Id, false, false);
};

StorageItemsWatcher.getLastChecked = function () {
  var check = Storage.get("ExpiringItems_LastChecked_" + Estiah.Player.Id);
  return check == null ? 0 : check;
};

StorageItemsWatcher.setLastChecked = function () {
  Storage.set(
    "ExpiringItems_LastChecked_" + Estiah.Player.Id,
    new Date().getTime().toString()
  );
};

StorageItemsWatcher.doNotRepeatYourself = false;

StorageItemsWatcher.Update = function (doc) {
  if (StorageItemsWatcher.doNotRepeatYourself) return; // called both from Check callback and Launcher / Read

  var expiring = doc.getElementsByClassName("clock_c2");
  // console.log(expiring.length + ' items expiring');
  var storedItems = StorageItemsWatcher.getItems();

  var toRemove = [];
  // console.log(doc.querySelectorAll('div.name a'));
  storedItems.foreach(function (name) {
    var item = doc.evaluate(
      '//*[@class="name" and *[text()=' +
        Web.SanitizeXpathName(name) +
        ']]/*[contains(@class, "clock")]',
      doc,
      null,
      XPathResult.FIRST_ORDERED_NODE_TYPE,
      null
    ).singleNodeValue;
    // console.log(item);
    if (item == null || item.getAttribute("class").indexOf("clock_c2") < 0) {
      toRemove.push(name);
    }
  });
  // console.log(toRemove);
  for (var r = 0; r < toRemove.length; r++) {
    // console.log('removing ' + toRemove[r]);
    storedItems.remove(toRemove[r]);
  }

  var lastChecked = StorageItemsWatcher.getLastChecked();
  for (var e = 0; e < expiring.length; e++) {
    var item = expiring[e].parentNode.parentNode;
    var nameEl = item.getElementsByClassName("BV_system_file")[0];
    var name = nameEl.innerHTML;
    if (!storedItems.contains(name)) {
      var rarity = nameEl.getAttribute("class").split(" ", 1);
      var count = item.querySelector("div.count strong").innerHTML;
      // console.log(name + ' ' + rarity + ' ' + count + ' ' + lastChecked);
      storedItems.insert(name, rarity, count, lastChecked);
    }
  }
  storedItems.save();
  StorageItemsWatcher.setLastChecked();
  StorageItemsWatcher.doNotRepeatYourself = true;
};

StorageItemsWatcher.CheckAndAlterPage = function (doc) {
  var lastChecked = StorageItemsWatcher.getLastChecked();
  if (new Date().getTime() - lastChecked > 1000 * 60 * 30 /* 30 minutes */) {
    Web.HttpRequest(
      "http://www.estiah.com/character/inventory",
      function (inv) {
        StorageItemsWatcher.Update(inv);
        StorageItemsWatcher.AlterPage(doc);
      }
    );
  } else StorageItemsWatcher.AlterPage(doc);
};

StorageItemsWatcher.AlterPage = function (doc) {
  var storedItems = StorageItemsWatcher.getItems();
  var count = 0;
  for (k in storedItems.map.container) count++;
  if (count > 0) {
    var menu = doc.getElementsByClassName("wireframe_menu")[0];
    Web.CreateElement("div", "separatorh bd1", menu);
    var entry = Web.CreateElement("div", "entry BV_menu_dropdown", menu);
    Web.CreateElement("a", "nolink disabled", entry).innerHTML =
      "Expiring (" + count + ")";
    var items = Web.CreateElement("div", "bd1 bg1 dropdown floating", entry);
    items.style.display = "none";

    var now = new Date().getTime();
    storedItems.foreach(function (name, info) {
      var row = Web.CreateElement(
        "div",
        "left",
        Web.CreateElement("div", "row", items)
      );
      Web.CreateElement("span", info[0], row).innerHTML = name;
      Web.appendText(" (" + info[1] + ") ", row);
      var timeRemaining =
        Math.floor((24 - (now - info[2]) / 1000 / 60 / 60) * 10) / 10;
      Web.CreateElement("span", "disabled", row).innerHTML =
        timeRemaining >= 0 ? timeRemaining + "+h" : "?";
    });
    if (unsafeWindow.System != undefined)
      unsafeWindow.System.register(unsafeWindow.Menu.rules);
  }
};

StorageItemsWatcher.MakeItAltFriendly = function () {
  var items = Storage.get("ExpiringItems");
  if (items != null) Storage.set("ExpiringItems_" + Estiah.Player.Id, items);
  var checked = Storage.get("ExpiringItems_LastChecked");
  if (checked != null)
    Storage.set("ExpiringItems_LastChecked_" + Estiah.Player.Id, checked);
  Storage.remove("ExpiringItems");
  Storage.remove("ExpiringItems_LastChecked");
};

Launcher.Register(
  "StorageItemsWatcher",
  "Watches for items that will expire soon. Check is performed once per 1/2h or by visiting inventory."
)
  .Read(Estiah.Inventory.UrlTest, StorageItemsWatcher.Update)
  .Modify(Launcher.UrlTest.EveryPage, StorageItemsWatcher.CheckAndAlterPage)
  .UpdateData("110301", StorageItemsWatcher.MakeItAltFriendly);

//////////////////
// Auto relogin //
//////////////////

AutoRelogin = {};

AutoRelogin.TryLogIn = null;
AutoRelogin.LogIn = function (doc) {
  var inputs = doc.querySelectorAll("#AuthForm .input");

  AutoRelogin.TryLogIn = function () {
    var filled = true;
    for (var i = 0; i < inputs.length; i++) {
      if (!inputs[i].value.length) filled = false;
    }
    if (filled) {
      // console.log(inputs[0].value + ':' + inputs[1].value);
      Web.MouseClick(doc.querySelector("#AuthSubmit a"));
    } else setTimeout(AutoRelogin.TryLogIn, 100);
  };

  AutoRelogin.TryLogIn();
};

Launcher.Register(
  "AutoRelogin",
  "If you have set autofill login/pass, it will refresh your lost session."
).Modify(Launcher.UrlTest.LogIn, AutoRelogin.LogIn);

////////////
////////////
/// Body ///
////////////
////////////

if (document.URL.indexOf("/rebuild") > -1) {
  DataUpdate.RemoveEmptyValues();
}

Launcher.Launch();