import { loadAliases, loadGlobalAliases, Aliases, GlobalAliases } from "./Alias";
import { Companies, loadCompanies } from "./Company/Companies";
import { CONSTANTS } from "./Constants";
import { Engine } from "./engine";
import { Factions, loadFactions } from "./Faction/Factions";
import { loadFconf } from "./Fconf/Fconf";
import { FconfSettings } from "./Fconf/FconfSettings";
import { loadAllGangs, AllGangs } from "./Gang/AllGangs";
import { loadMessages, initMessages, Messages } from "./Message/MessageHelpers";
import { Player, loadPlayer } from "./Player";
import { AllServers, loadAllServers } from "./Server/AllServers";
import { Settings } from "./Settings/Settings";
import { loadSpecialServerIps, SpecialServerIps } from "./Server/SpecialServerIps";
import { SourceFileFlags } from "./SourceFile/SourceFileFlags";
import { loadStockMarket, StockMarket } from "./StockMarket/StockMarket";

import { createStatusText } from "./ui/createStatusText";

import { setTimeoutRef } from "./utils/SetTimeoutRef";
import * as ExportBonus from "./ExportBonus";

import { dialogBoxCreate } from "../utils/DialogBox";
import { clearEventListeners } from "../utils/uiHelpers/clearEventListeners";
import { Reviver, Generic_toJSON, Generic_fromJSON } from "../utils/JSONReviver";

import Decimal from "decimal.js";

/* SaveObject.js
 *  Defines the object used to save/load games
 */
let saveObject = new BitburnerSaveObject();

function BitburnerSaveObject() {
  this.PlayerSave = "";
  this.AllServersSave = "";
  this.CompaniesSave = "";
  this.FactionsSave = "";
  this.SpecialServerIpsSave = "";
  this.AliasesSave = "";
  this.GlobalAliasesSave = "";
  this.MessagesSave = "";
  this.StockMarketSave = "";
  this.SettingsSave = "";
  this.FconfSettingsSave = "";
  this.VersionSave = "";
  this.AllGangsSave = "";
  this.LastExportBonus = "";
}

BitburnerSaveObject.prototype.getSaveString = function () {
  this.PlayerSave = JSON.stringify(Player);

  // Delete all logs from all running scripts
  var TempAllServers = JSON.parse(JSON.stringify(AllServers), Reviver);
  for (var ip in TempAllServers) {
    var server = TempAllServers[ip];
    if (server == null) {
      continue;
    }
    for (var i = 0; i < server.runningScripts.length; ++i) {
      var runningScriptObj = server.runningScripts[i];
      runningScriptObj.logs.length = 0;
      runningScriptObj.logs = [];
    }
  }

  this.AllServersSave = JSON.stringify(TempAllServers);
  this.CompaniesSave = JSON.stringify(Companies);
  this.FactionsSave = JSON.stringify(Factions);
  this.SpecialServerIpsSave = JSON.stringify(SpecialServerIps);
  this.AliasesSave = JSON.stringify(Aliases);
  this.GlobalAliasesSave = JSON.stringify(GlobalAliases);
  this.MessagesSave = JSON.stringify(Messages);
  this.StockMarketSave = JSON.stringify(StockMarket);
  this.SettingsSave = JSON.stringify(Settings);
  this.FconfSettingsSave = JSON.stringify(FconfSettings);
  this.VersionSave = JSON.stringify(CONSTANTS.Version);
  this.LastExportBonus = JSON.stringify(ExportBonus.LastExportBonus);
  if (Player.inGang()) {
    this.AllGangsSave = JSON.stringify(AllGangs);
  }
  var saveString = btoa(unescape(encodeURIComponent(JSON.stringify(this))));

  return saveString;
};

BitburnerSaveObject.prototype.saveGame = function (db) {
  var saveString = this.getSaveString();

  // We'll save to both localstorage and indexedDb
  var objectStore = db.transaction(["savestring"], "readwrite").objectStore("savestring");
  var request = objectStore.put(saveString, "save");

  request.onerror = function (e) {
    console.error("Error saving game to IndexedDB: " + e);
  };

  try {
    window.localStorage.setItem("bitburnerSave", saveString);
  } catch (e) {
    if (e.code == 22) {
      createStatusText("Save failed for localStorage! Check console(F12)");
      console.error(
        "Failed to save game to localStorage because the size of the save file " +
          "is too large. However, the game will still be saved to IndexedDb if your browser " +
          "supports it. If you would like to save to localStorage as well, then " +
          "consider killing several of your scripts to " +
          "fix this, or increasing the size of your browsers localStorage",
      );
    }
  }

  createStatusText("Game saved!");
};

// Makes necessary changes to the loaded/imported data to ensure
// the game stills works with new versions
function evaluateVersionCompatibility(ver) {
  // This version refactored the Company/job-related code
  if (ver <= "0.41.2") {
    // Player's company position is now a string
    if (Player.companyPosition != null && typeof Player.companyPosition !== "string") {
      console.log("Changed Player.companyPosition value to be compatible with v0.41.2");
      Player.companyPosition = Player.companyPosition.data.positionName;
      if (Player.companyPosition == null) {
        Player.companyPosition = "";
      }
    }

    // The "companyName" property of all Companies is renamed to "name"
    for (var companyName in Companies) {
      const company = Companies[companyName];
      if ((company.name == null || company.name === 0 || company.name === "") && company.companyName != null) {
        console.log("Changed company name property to be compatible with v0.41.2");
        company.name = company.companyName;
      }

      if (company.companyPositions instanceof Array) {
        console.log("Changed company companyPositions property to be compatible with v0.41.2");
        const pos = {};

        for (let i = 0; i < company.companyPositions.length; ++i) {
          pos[company.companyPositions[i]] = true;
        }
        company.companyPositions = pos;
      }
    }
  }

  // This version allowed players to hold multiple jobs
  if (ver < "0.43.0") {
    if (Player.companyName !== "" && Player.companyPosition != null && Player.companyPosition !== "") {
      console.log("Copied player's companyName and companyPosition properties to the Player.jobs map for v0.43.0");
      Player.jobs[Player.companyName] = Player.companyPosition;
    }

    delete Player.companyPosition;
  }
}

function loadGame(saveString) {
  if (saveString === "" || saveString == null || saveString === undefined) {
    if (!window.localStorage.getItem("bitburnerSave")) {
      console.log("No save file to load");
      return false;
    }
    saveString = decodeURIComponent(escape(atob(window.localStorage.getItem("bitburnerSave"))));
    console.log("Loading game from localStorage");
  } else {
    saveString = decodeURIComponent(escape(atob(saveString)));
    console.log("Loading game from IndexedDB");
  }

  var saveObj = JSON.parse(saveString, Reviver);

  loadPlayer(saveObj.PlayerSave);
  loadAllServers(saveObj.AllServersSave);
  loadCompanies(saveObj.CompaniesSave);
  loadFactions(saveObj.FactionsSave);
  loadSpecialServerIps(saveObj.SpecialServerIpsSave);

  if (saveObj.hasOwnProperty("AliasesSave")) {
    try {
      loadAliases(saveObj.AliasesSave);
    } catch (e) {
      console.warn(`Could not load Aliases from save`);
      loadAliases("");
    }
  } else {
    console.warn(`Save file did not contain an Aliases property`);
    loadAliases("");
  }
  if (saveObj.hasOwnProperty("GlobalAliasesSave")) {
    try {
      loadGlobalAliases(saveObj.GlobalAliasesSave);
    } catch (e) {
      console.warn(`Could not load GlobalAliases from save`);
      loadGlobalAliases("");
    }
  } else {
    console.warn(`Save file did not contain a GlobalAliases property`);
    loadGlobalAliases("");
  }
  if (saveObj.hasOwnProperty("MessagesSave")) {
    try {
      loadMessages(saveObj.MessagesSave);
    } catch (e) {
      console.warn(`Could not load Messages from save`);
      initMessages();
    }
  } else {
    console.warn(`Save file did not contain a Messages property`);
    initMessages();
  }
  if (saveObj.hasOwnProperty("StockMarketSave")) {
    try {
      loadStockMarket(saveObj.StockMarketSave);
    } catch (e) {
      loadStockMarket("");
    }
  } else {
    loadStockMarket("");
  }
  if (saveObj.hasOwnProperty("SettingsSave")) {
    try {
      Settings.load(saveObj.SettingsSave);
    } catch (e) {
      console.error("ERROR: Failed to parse Settings. Re-initing default values");
      Settings.init();
    }
  } else {
    Settings.init();
  }
  if (saveObj.hasOwnProperty("FconfSettingsSave")) {
    try {
      loadFconf(saveObj.FconfSettingsSave);
    } catch (e) {
      console.error("ERROR: Failed to parse .fconf Settings.");
    }
  }
  if (saveObj.hasOwnProperty("LastExportBonus")) {
    try {
      ExportBonus.setLastExportBonus(JSON.parse(saveObj.LastExportBonus));
    } catch (err) {
      ExportBonus.setLastExportBonus(new Date().getTime());
      console.error("ERROR: Failed to parse .fconf Settings " + err);
    }
  }
  if (saveObj.hasOwnProperty("VersionSave")) {
    try {
      var ver = JSON.parse(saveObj.VersionSave, Reviver);
      evaluateVersionCompatibility(ver);

      if (window.location.href.toLowerCase().includes("bitburner-beta")) {
        // Beta branch, always show changes
        createBetaUpdateText();
      } else if (ver != CONSTANTS.Version) {
        createNewUpdateText();
      }
    } catch (e) {
      createNewUpdateText();
    }
  } else {
    createNewUpdateText();
  }
  if (Player.inGang() && saveObj.hasOwnProperty("AllGangsSave")) {
    try {
      loadAllGangs(saveObj.AllGangsSave);
    } catch (e) {
      console.error("ERROR: Failed to parse AllGangsSave: " + e);
    }
  }

  return true;
}

function loadImportedGame(saveObj, saveString) {
  var tempSaveObj = null;
  var tempPlayer = null;

  // Check to see if the imported save file can be parsed. If any
  // errors are caught it will fail
  try {
    var decodedSaveString = decodeURIComponent(escape(atob(saveString)));
    tempSaveObj = JSON.parse(decodedSaveString, Reviver);

    tempPlayer = JSON.parse(tempSaveObj.PlayerSave, Reviver);

    // Parse Decimal.js objects
    tempPlayer.money = new Decimal(tempPlayer.money);

    JSON.parse(tempSaveObj.AllServersSave, Reviver);
    JSON.parse(tempSaveObj.CompaniesSave, Reviver);
    JSON.parse(tempSaveObj.FactionsSave, Reviver);
    JSON.parse(tempSaveObj.SpecialServerIpsSave, Reviver);
    if (tempSaveObj.hasOwnProperty("AliasesSave")) {
      try {
        JSON.parse(tempSaveObj.AliasesSave, Reviver);
      } catch (e) {
        console.error(`Parsing Aliases save failed: ${e}`);
      }
    }
    if (tempSaveObj.hasOwnProperty("GlobalAliases")) {
      try {
        JSON.parse(tempSaveObj.AliasesSave, Reviver);
      } catch (e) {
        console.error(`Parsing Global Aliases save failed: ${e}`);
      }
    }
    if (tempSaveObj.hasOwnProperty("MessagesSave")) {
      try {
        JSON.parse(tempSaveObj.MessagesSave, Reviver);
      } catch (e) {
        console.error(`Parsing Messages save failed: ${e}`);
        initMessages();
      }
    } else {
      initMessages();
    }
    if (saveObj.hasOwnProperty("StockMarketSave")) {
      try {
        JSON.parse(tempSaveObj.StockMarketSave, Reviver);
      } catch (e) {
        console.error(`Parsing StockMarket save failed: ${e}`);
      }
    }
    if (saveObj.hasOwnProperty("LastExportBonus")) {
      try {
        ExportBonus.setLastExportBonus(JSON.parse(saveObj.LastExportBonus));
      } catch (err) {
        ExportBonus.setLastExportBonus(new Date().getTime());
        console.error("ERROR: Failed to parse .fconf Settings " + err);
      }
    }
    if (tempSaveObj.hasOwnProperty("VersionSave")) {
      try {
        var ver = JSON.parse(tempSaveObj.VersionSave, Reviver);
        evaluateVersionCompatibility(ver);
      } catch (e) {
        console.error("Parsing Version save failed: " + e);
      }
    }
    if (tempPlayer.inGang() && tempSaveObj.hasOwnProperty("AllGangsSave")) {
      try {
        loadAllGangs(tempSaveObj.AllGangsSave);
      } catch (e) {
        console.error(`Failed to parse AllGangsSave: {e}`);
        throw e;
      }
    }
  } catch (e) {
    dialogBoxCreate("Error importing game: " + e.toString());
    return false;
  }

  // Since the save file is valid, load everything for real
  saveString = decodeURIComponent(escape(atob(saveString)));
  saveObj = JSON.parse(saveString, Reviver);

  loadPlayer(saveObj.PlayerSave);
  loadAllServers(saveObj.AllServersSave);
  loadCompanies(saveObj.CompaniesSave);
  loadFactions(saveObj.FactionsSave);
  loadSpecialServerIps(saveObj.SpecialServerIpsSave);

  if (saveObj.hasOwnProperty("AliasesSave")) {
    try {
      loadAliases(saveObj.AliasesSave);
    } catch (e) {
      loadAliases("");
    }
  } else {
    loadAliases("");
  }
  if (saveObj.hasOwnProperty("GlobalAliasesSave")) {
    try {
      loadGlobalAliases(saveObj.GlobalAliasesSave);
    } catch (e) {
      loadGlobalAliases("");
    }
  } else {
    loadGlobalAliases("");
  }
  if (saveObj.hasOwnProperty("MessagesSave")) {
    try {
      loadMessages(saveObj.MessagesSave);
    } catch (e) {
      initMessages();
    }
  } else {
    initMessages();
  }
  if (saveObj.hasOwnProperty("StockMarketSave")) {
    try {
      loadStockMarket(saveObj.StockMarketSave);
    } catch (e) {
      loadStockMarket("");
    }
  } else {
    loadStockMarket("");
  }
  if (saveObj.hasOwnProperty("SettingsSave")) {
    try {
      Settings.load(saveObj.SettingsSave);
    } catch (e) {
      Settings.init();
    }
  } else {
    Settings.init();
  }
  if (saveObj.hasOwnProperty("FconfSettingsSave")) {
    try {
      loadFconf(saveObj.FconfSettingsSave);
    } catch (e) {
      console.error("ERROR: Failed to load .fconf settings when importing");
    }
  }
  if (saveObj.hasOwnProperty("VersionSave")) {
    try {
      var ver = JSON.parse(saveObj.VersionSave, Reviver);
      evaluateVersionCompatibility(ver);

      if (ver != CONSTANTS.Version) {
        createNewUpdateText();
      }
    } catch (e) {
      createNewUpdateText();
    }
  } else {
    createNewUpdateText();
  }
  if (Player.inGang() && saveObj.hasOwnProperty("AllGangsSave")) {
    try {
      loadAllGangs(saveObj.AllGangsSave);
    } catch (e) {
      console.error("ERROR: Failed to parse AllGangsSave: " + e);
    }
  }
  saveObject.saveGame(Engine.indexedDb);
  location.reload();
  return true;
}

BitburnerSaveObject.prototype.exportGame = function () {
  const saveString = this.getSaveString();

  // Save file name is based on current timestamp and BitNode
  const epochTime = Math.round(Date.now() / 1000);
  const bn = Player.bitNodeN;
  const filename = `bitburnerSave_BN${bn}x${SourceFileFlags[bn]}_${epochTime}.json`;
  var file = new Blob([saveString], { type: "text/plain" });
  if (window.navigator.msSaveOrOpenBlob) {
    // IE10+
    window.navigator.msSaveOrOpenBlob(file, filename);
  } else {
    // Others
    var a = document.createElement("a"),
      url = URL.createObjectURL(file);
    a.href = url;
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    setTimeoutRef(function () {
      document.body.removeChild(a);
      window.URL.revokeObjectURL(url);
    }, 0);
  }
};

BitburnerSaveObject.prototype.importGame = function () {
  if (window.File && window.FileReader && window.FileList && window.Blob) {
    var fileSelector = clearEventListeners("import-game-file-selector");
    fileSelector.addEventListener("change", openImportFileHandler, false);
    $("#import-game-file-selector").click();
  } else {
    dialogBoxCreate("ERR: Your browser does not support HTML5 File API. Cannot import.");
  }
};

BitburnerSaveObject.prototype.deleteGame = function (db) {
  // Delete from local storage
  if (window.localStorage.getItem("bitburnerSave")) {
    window.localStorage.removeItem("bitburnerSave");
  }

  // Delete from indexedDB
  var request = db.transaction(["savestring"], "readwrite").objectStore("savestring").delete("save");
  request.onsuccess = function () {
    console.log("Successfully deleted save from indexedDb");
  };
  request.onerror = function (e) {
    console.error(`Failed to delete save from indexedDb: ${e}`);
  };
  createStatusText("Game deleted!");
};

function createNewUpdateText() {
  dialogBoxCreate(
    "New update!<br>" +
      "Please report any bugs/issues through the github repository " +
      "or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
      CONSTANTS.LatestUpdate,
  );
}

function createBetaUpdateText() {
  dialogBoxCreate(
    "You are playing on the beta environment! This branch of the game " +
      "features the latest developments in the game. This version may be unstable.<br>" +
      "Please report any bugs/issues through the github repository (https://github.com/danielyxie/bitburner/issues) " +
      "or the Bitburner subreddit (reddit.com/r/bitburner).<br><br>" +
      CONSTANTS.LatestUpdate,
  );
}

BitburnerSaveObject.prototype.toJSON = function () {
  return Generic_toJSON("BitburnerSaveObject", this);
};

BitburnerSaveObject.fromJSON = function (value) {
  return Generic_fromJSON(BitburnerSaveObject, value.data);
};

Reviver.constructors.BitburnerSaveObject = BitburnerSaveObject;

function openImportFileHandler(evt) {
  var file = evt.target.files[0];
  if (!file) {
    dialogBoxCreate("Invalid file selected");
    return;
  }

  var reader = new FileReader();
  reader.onload = function (e) {
    var contents = e.target.result;
    loadImportedGame(saveObject, contents);
  };
  reader.readAsText(file);
}

export { saveObject, loadGame };
