/**
 * server/src/world/worldloader.cpp
 *
 * This file is part of the traintastic source code.
 *
 * Copyright (C) 2019-2025 Reinder Feenstra
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "worldloader.hpp"
#include <fstream>
#include <boost/algorithm/string.hpp>
#include <boost/uuid/string_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
#include "world.hpp"
#include "../core/isvalidobjectid.hpp"
#include "../utils/startswith.hpp"
#include "../utils/stripsuffix.hpp"
#include "ctwreader.hpp"
#include "../log/logmessageexception.hpp"
#include <version.hpp>

#include "../board/board.hpp"
#include "../board/tile/tiles.hpp"
#include "../hardware/interface/interfaces.hpp"
#include "../hardware/interface/dccexinterface.hpp" //! \todo Remove in v0.4
#include "../hardware/decoder/decoder.hpp"
#include "../hardware/decoder/decoderfunction.hpp"
#include "../hardware/identification/identification.hpp"
#include "../hardware/booster/booster.hpp"
#include "../vehicle/rail/railvehicles.hpp"
#include "../vehicle/rail/freightwagon.hpp" //! \todo Remove in v0.4
#include "../train/train.hpp"
#include "../train/trainblockstatus.hpp"
#include "../lua/script.hpp"
#include "../zone/zone.hpp"

using nlohmann::json;

WorldLoader::WorldLoader()
  : m_world{World::create()}
{
}

WorldLoader::WorldLoader(std::filesystem::path path)
  : WorldLoader()
{
  if(path.extension() == World::dotCTW)
    m_ctw = std::make_unique<CTWReader>(path);
  else
    m_path = std::move(path);

  load();
}

WorldLoader::WorldLoader(const std::vector<std::byte>& memory)
  : WorldLoader()
{
  m_ctw = std::make_unique<CTWReader>(memory);

  load();
}

WorldLoader::~WorldLoader() = default; // default here, so we can use a forward declaration of CTWReader in the header.

ObjectPtr WorldLoader::getObject(std::string_view id)
{
  std::vector<std::string> ids;
  boost::split(ids, id, [](char c){ return c == '.'; });
  auto itId = ids.cbegin();

  ObjectPtr obj;
  if(auto it = m_objects.find(*itId); it != m_objects.end())
  {
    if(!it->second.object)
      createObject(it->second);
    obj = it->second.object;
  }

  while(obj && ++itId != ids.cend())
  {
    AbstractProperty* property = obj->getProperty(*itId);
    if(property && property->type() == ValueType::Object)
      obj = property->toObject();
    else
      obj = nullptr;
  }

  return obj;
}

json WorldLoader::getState(const std::string& id) const
{
  return m_states.value(id, json::object());
}

void WorldLoader::load()
{
  m_states = json::object();

  json data;
  json state;

  // load file(s):
  if(m_ctw)
  {
    if(!m_ctw->readFile(World::filename, data))
      throw std::runtime_error(std::string("can't read ").append(World::filename));

    if(!m_ctw->readFile(World::filenameState, state))
      throw std::runtime_error(std::string("can't read ").append(World::filenameState));
  }
  else
  {
    std::ifstream file(m_path / World::filename);
    if(!file.is_open())
      throw std::runtime_error("can't open " + (m_path / World::filename).string());
    data = json::parse(file);

    std::ifstream stateFile(m_path / World::filenameState);
    if(!stateFile.is_open())
      throw std::runtime_error("can't open " + (m_path / World::filenameState).string());
    state = json::parse(stateFile);
  }

  // check if UUID is valid:
  m_world->uuid.setValueInternal(to_string(boost::uuids::string_generator()(std::string(data["uuid"]))));

  // check version:
  //! \todo require this for >= v0.2
  {
    json traintastic = data["traintastic"];
    if(traintastic.is_object())
    {
      json version = traintastic["version"];
      if(version.is_object())
      {
        const uint16_t major = version["major"].get<uint16_t>();
        const uint16_t minor = version["minor"].get<uint16_t>();
        const uint16_t patch = version["patch"].get<uint16_t>();

        if((major != TRAINTASTIC_VERSION_MAJOR) ||
            (minor > TRAINTASTIC_VERSION_MINOR) ||
            (minor == TRAINTASTIC_VERSION_MINOR && patch > TRAINTASTIC_VERSION_PATCH))
        {
          throw LogMessageException(LogMessage::C1013_CANT_LOAD_WORLD_SAVED_WITH_NEWER_VERSION_REQUIRES_AT_LEAST_X,
              std::string("Traintastic server v").append(std::to_string(major)).append(".").append(std::to_string(minor)).append(".").append(std::to_string(patch)));
        }
      }
    }
  }

  // state data
  if(state.is_object() && state["uuid"] == data["uuid"])
  {
    m_states = state["states"];
    auto stateObjects = state.value("objects", json::array());
    data["objects"].insert(data["objects"].end(), stateObjects.begin(), stateObjects.end());
  }

  // create a list of all objects
  m_objects.insert({m_world->getObjectId(), {data, m_world, false}});
  for(json object : data["objects"])
  {
    //! \todo Remove in v0.4
    if(object["class_id"].get<std::string_view>() == "output") // don't create Output objects, no longer stored in file.
    {
      continue;
    }

    if(auto it = object.find("id"); it != object.end())
    {
      auto id = it.value().get<std::string>();
      if(!isValidObjectId(id))
        throw std::runtime_error("invalid object id value");
      m_objects.insert({std::move(id), {object, nullptr, false}});
    }
    else
      throw std::runtime_error("id missing");
  }

  //! \todo Remove in v0.4
  {
    // patch for input refactor:
    for(auto& objectData : m_objects)
    {
      if(!objectData.second.json.contains("class_id"))
      {
        continue;
      }
      auto classId = objectData.second.json["class_id"].get<std::string_view>();
      if(classId == "board_tile.rail.sensor" ||
          classId == "board_tile.rail.nx_button" ||
          classId == "input_map_item.block")
      {
        const auto& input = objectData.second.json["input"];
        if(input.is_string())
        {
          if(auto it = m_objects.find(input.get<std::string>()); it != m_objects.end()) [[likely]]
          {
            objectData.second.json["interface"] = it->second.json["interface"];
            objectData.second.json["channel"] = it->second.json["channel"];
            objectData.second.json["address"] = it->second.json["address"];
          }
        }
        objectData.second.json.erase("input");
      }
      else if(classId == "board_tile.rail.block")
      {
        auto& inputMap = objectData.second.json["input_map"];
        if(inputMap.is_object())
        {
          auto& items = inputMap["items"];
          if(items.is_array())
          {
            for(auto& item : items)
            {
              const auto& input = item["input"];
              if(input.is_string())
              {
                if(auto it = m_objects.find(input.get<std::string>()); it != m_objects.end()) [[likely]]
                {
                  item["interface"] = it->second.json["interface"];
                  const int ch = it->second.json["channel"].get<int>();
                  if(ch == 0)
                  {
                    item["channel"] = "input";
                  }
                  else if(auto interface = m_objects.find(item["interface"].get<std::string>()); it != m_objects.end()) [[likely]]
                  {
                    const auto interfaceClassId = interface->second.json["class_id"].get<std::string_view>();
                    if(interfaceClassId == "interface.ecos")
                    {
                      if(ch == 1)
                      {
                        item["channel"] = "s88";
                      }
                      else if(ch == 2)
                      {
                        item["channel"] = "ecos_detector";
                      }
                      else [[unlikely]]
                      {
                        assert(false);
                      }
                    }
                    else if(interfaceClassId == "interface.hsi88")
                    {
                      if(ch == 1)
                      {
                        item["channel"] = "s88_left";
                      }
                      else if(ch == 2)
                      {
                        item["channel"] = "s88_middle";
                      }
                      else if(ch == 3)
                      {
                        item["channel"] = "s88_middle";
                      }
                      else [[unlikely]]
                      {
                        assert(false);
                      }
                    }
                    else if(interfaceClassId == "interface.z21")
                    {
                      if(ch == 1)
                      {
                        item["channel"] = "rbus";
                      }
                      else if(ch == 2)
                      {
                        item["channel"] = "loconet";
                      }
                      else [[unlikely]]
                      {
                        assert(false);
                      }
                    }
                    else [[unlikely]]
                    {
                      assert(false);
                    }
                  }
                  item["address"] = it->second.json["address"];
                }
              }
              item.erase("input");
            }
          }
        }
      }
    }
    // remove all input objects:
    for(auto it = m_objects.begin(); it != m_objects.end();)
    {
      if(it->second.json.contains("class_id") && it->second.json["class_id"].get<std::string_view>() == "input")
      {
        it = m_objects.erase(it);
      }
      else
      {
        it++;
      }
    }
  }

  // then create all objects
  for(auto& it : m_objects)
    if(!it.second.object)
      createObject(it.second);

  // and load their data/state
  for(auto& it : m_objects)
    if(!it.second.loaded)
      loadObject(it.second);

  // and finally notify loading is completed
  for(auto& it : m_objects)
    it.second.object->loaded();
}

void WorldLoader::createObject(ObjectData& objectData)
{
  assert(!objectData.object);

  std::string_view classId = objectData.json["class_id"].get<std::string_view>();
  std::string_view id = objectData.json["id"].get<std::string_view>();

  if(startsWith(classId, Interfaces::classIdPrefix))
  {
    if(classId == "interface.dccplusplus") //! \todo Remove in v0.4
    {
      objectData.json["dccex"] = objectData.json["dccplusplus"];
      objectData.json["dccex"]["class_id"] = "dccex_settings";
      objectData.json.erase("dccplusplus");
      classId = DCCEXInterface::classId;
    }
    objectData.object = Interfaces::create(*m_world, classId, id);
  }
  else if(classId == Decoder::classId)
  {
    if(objectData.json["protocol"].get<std::string_view>() == "dcc") //! \todo Remove in v0.4
    {
      if(objectData.json["long_address"].get<bool>())
        objectData.json["protocol"] = "dcc_long";
      else
        objectData.json["protocol"] = "dcc_short";
    }
    objectData.object = Decoder::create(*m_world, id);
  }
  else if(classId == Identification::classId)
    objectData.object = Identification::create(*m_world, id);
  else if(classId == Booster::classId)
  {
    objectData.object = Booster::create(*m_world, id);
  }
  else if(classId == Board::classId)
    objectData.object = Board::create(*m_world, id);
  else if(startsWith(classId, Tiles::classIdPrefix))
  {
    if(auto tile = Tiles::create(*m_world, classId, id))
    {
      // x, y, width, height are read in Board::load()
      tile->x.setValueInternal(objectData.json["x"]);
      tile->y.setValueInternal(objectData.json["y"]);
      tile->height.setValueInternal(objectData.json.value("height", 1));
      tile->width.setValueInternal(objectData.json.value("width", 1));
      objectData.object = tile;
    }
  }
  else if(startsWith(classId, RailVehicles::classIdPrefix))
  {
    if(classId == "vehicle.rail.freight_car") { classId = FreightWagon::classId; } //! \todo Remove in v0.4
    if(objectData.json.contains("lob") && !objectData.json.contains("length")) //! \todo Remove in v0.4
    {
      objectData.json["length"] = objectData.json["lob"];
      objectData.json.erase("lob");
    }
    objectData.object = RailVehicles::create(*m_world, classId, id);
  }
  else if(classId == Train::classId)
  {
    if(objectData.json.contains("lob") && !objectData.json.contains("length")) //! \todo Remove in v0.4
    {
      objectData.json["length"] = objectData.json["lob"];
      objectData.json.erase("lob");
    }
    objectData.object = Train::create(*m_world, id);
  }
  else if(classId == TrainBlockStatus::classId)
  {
    auto block = std::dynamic_pointer_cast<BlockRailTile>(getObject(objectData.json["block"].get<std::string_view>()));

    if(block) /*[[likely]]*/
    {
      if(objectData.json["train"].is_string())
      {
        auto train = std::dynamic_pointer_cast<Train>(getObject(objectData.json["train"].get<std::string_view>()));
        objectData.object = TrainBlockStatus::create(*block, *train, to<BlockTrainDirection>(objectData.json["direction"]), id);
      }
      else
      {
        objectData.object = TrainBlockStatus::create(*block, objectData.json["identification"].get<std::string>(), to<BlockTrainDirection>(objectData.json["direction"]), id);
      }
    }
  }
  else if(classId == TrainZoneStatus::classId)
  {
    if(auto zone = std::dynamic_pointer_cast<Zone>(getObject(objectData.json["zone"].get<std::string_view>()))) /*[[likely]]*/
    {
      auto train = std::dynamic_pointer_cast<Train>(getObject(objectData.json["train"].get<std::string_view>()));
      objectData.object = TrainZoneStatus::create(*zone, *train, to<ZoneTrainState>(objectData.json["state"]), id);
    }
  }
  else if(classId == Lua::Script::classId)
    objectData.object = Lua::Script::create(*m_world, id);
  else if(classId == Zone::classId)
  {
    objectData.object = Zone::create(*m_world, id);
  }

  if(!objectData.object)
    throw LogMessageException(LogMessage::C1012_UNKNOWN_CLASS_X_CANT_RECREATE_OBJECT_X, classId, id);
}

void WorldLoader::loadObject(ObjectData& objectData)
{
  assert(objectData.object);
  assert(!objectData.loaded);
  objectData.object->load(*this, objectData.json);
  objectData.loaded = true;
}

bool WorldLoader::readFile(const std::filesystem::path& filename, std::string& data)
{
  if(m_ctw)
  {
    if(!m_ctw->readFile(filename, data))
      return false;
  }
  else
  {
    std::ifstream file(m_path / filename, std::ios::in | std::ios::binary | std::ios::ate);
    if(!file.is_open())
      return false;
    const size_t size = file.tellg();
    data.resize(size);
    file.seekg(std::ios::beg);
    file.read(data.data(), size);
  }
  return true;
}

bool WorldLoader::readFile(const std::filesystem::path& filename, nlohmann::json& data)
{
  std::string text;
  if(readFile(filename, text))
  {
    try
    {
      data = nlohmann::json::parse(text);
      return true;
    }
    catch(...)
    {
    }
  }
  return false;
}
