/**
 * server/src/world/world.cpp
 *
 * This file is part of the traintastic source code.
 *
 * Copyright (C) 2019-2023 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 "world.hpp"
#include <fstream>
#include <iomanip>
#include <boost/algorithm/string.hpp>
#include <boost/uuid/random_generator.hpp>
#include <boost/uuid/string_generator.hpp>
#include <boost/uuid/uuid_io.hpp>
#include "worldsaver.hpp"
#include "../board/tile/rail/linkrailtile.hpp"
#include "../core/objectlisttablemodel.hpp"
#include "../core/attributes.hpp"
#include "../core/abstractvectorproperty.hpp"
#include "../hardware/input/input.hpp"
#include "../hardware/identification/identification.hpp"
#include "../hardware/programming/lncv/lncvprogrammer.hpp"
#include "../log/log.hpp"
#include "../utils/displayname.hpp"
#include "../traintastic/traintastic.hpp"

using nlohmann::json;

constexpr auto decoderListColumns = DecoderListColumn::Id | DecoderListColumn::Name | DecoderListColumn::Interface | DecoderListColumn::Address;
constexpr auto inputListColumns = InputListColumn::Id | InputListColumn::Name | InputListColumn::Interface | InputListColumn::Channel | InputListColumn::Address;
constexpr auto outputListColumns = OutputListColumn::Id | OutputListColumn::Name | OutputListColumn::Interface | OutputListColumn::Channel | OutputListColumn::Address;
constexpr auto identificationListColumns = IdentificationListColumn::Id | IdentificationListColumn::Name | IdentificationListColumn::Interface /*| IdentificationListColumn::Channel*/ | IdentificationListColumn::Address;

template<class T>
inline static void removeAll(T& objectList)
{
  while(!objectList.empty())
    objectList.remove(objectList.front());
}

std::shared_ptr<World> World::create()
{
  auto world = std::make_shared<World>(Private());
  init(*world);
  return world;
}

void World::init(World& world)
{
  world.decoderControllers.setValueInternal(std::make_shared<ControllerList<DecoderController>>(world, world.decoderControllers.name()));
  world.inputControllers.setValueInternal(std::make_shared<ControllerList<InputController>>(world, world.inputControllers.name()));
  world.outputControllers.setValueInternal(std::make_shared<ControllerList<OutputController>>(world, world.outputControllers.name()));
  world.identificationControllers.setValueInternal(std::make_shared<ControllerList<IdentificationController>>(world, world.identificationControllers.name()));
  world.lncvProgrammingControllers.setValueInternal(std::make_shared<ControllerList<LNCVProgrammingController>>(world, world.lncvProgrammingControllers.name()));

  world.interfaces.setValueInternal(std::make_shared<InterfaceList>(world, world.interfaces.name()));
  world.decoders.setValueInternal(std::make_shared<DecoderList>(world, world.decoders.name(), decoderListColumns));
  world.inputs.setValueInternal(std::make_shared<InputList>(world, world.inputs.name(), inputListColumns));
  world.outputs.setValueInternal(std::make_shared<OutputList>(world, world.outputs.name(), outputListColumns));
  world.identifications.setValueInternal(std::make_shared<IdentificationList>(world, world.outputs.name(), identificationListColumns));
  world.boards.setValueInternal(std::make_shared<BoardList>(world, world.boards.name()));
  world.clock.setValueInternal(std::make_shared<Clock>(world, world.clock.name()));
  world.trains.setValueInternal(std::make_shared<TrainList>(world, world.trains.name()));
  world.railVehicles.setValueInternal(std::make_shared<RailVehicleList>(world, world.railVehicles.name()));
  world.luaScripts.setValueInternal(std::make_shared<Lua::ScriptList>(world, world.luaScripts.name()));

  world.linkRailTiles.setValueInternal(std::make_shared<LinkRailTileList>(world, world.linkRailTiles.name()));
}

World::World(Private /*unused*/) :
  uuid{this, "uuid", to_string(boost::uuids::random_generator()()), PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
  name{this, "name", "", PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
  scale{this, "scale", WorldScale::H0, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly, [this](WorldScale /*value*/){ updateScaleRatio(); }},
  scaleRatio{this, "scale_ratio", 87, PropertyFlags::ReadWrite | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
  decoderControllers{this, "input_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  inputControllers{this, "input_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  outputControllers{this, "output_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  identificationControllers{this, "identification_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  lncvProgrammingControllers{this, "lncv_programming_controllers", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  interfaces{this, "interfaces", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  decoders{this, "decoders", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  inputs{this, "inputs", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  outputs{this, "outputs", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  identifications{this, "identifications", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  boards{this, "boards", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
  clock{this, "clock", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::Store | PropertyFlags::ScriptReadOnly},
  trains{this, "trains", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
  railVehicles{this, "rail_vehicles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
  luaScripts{this, "lua_scripts", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  linkRailTiles{this, "link_rail_tiles", nullptr, PropertyFlags::ReadOnly | PropertyFlags::SubObject | PropertyFlags::NoStore},
  hardwareThrottles{this, "hardware_throttles", 0, PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::NoScript},
  state{this, "state", WorldState(), PropertyFlags::ReadOnly | PropertyFlags::NoStore | PropertyFlags::ScriptReadOnly},
  edit{this, "edit", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore,
    [this](bool value)
    {
      if(value)
      {
        Log::log(*this, LogMessage::N1010_EDIT_MODE_ENABLED);
        state.setValueInternal(state.value() + WorldState::Edit);
        event(WorldEvent::EditEnabled);
      }
      else
      {
        Log::log(*this, LogMessage::N1011_EDIT_MODE_DISABLED);
        state.setValueInternal(state.value() - WorldState::Edit);
        event(WorldEvent::EditDisabled);
      }
    }},
  offline{*this, "offline",
    [this]()
    {
      event(WorldEvent::Offline);
    }},
  online{*this, "online",
    [this]()
    {
      event(WorldEvent::Online);
    }},
  powerOff{*this, "power_off", MethodFlags::ScriptCallable,
    [this]()
    {
      event(WorldEvent::PowerOff);
    }},
  powerOn{*this, "power_on",
    [this]()
    {
      event(WorldEvent::PowerOn);
    }},
  run{*this, "run",
    [this]()
    {
      event(WorldEvent::Run);
    }},
  stop{*this, "stop", MethodFlags::ScriptCallable,
    [this]()
    {
      event(WorldEvent::Stop);
    }},
  mute{this, "mute", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore,
    [this](bool value)
    {
      event(value ? WorldEvent::Mute : WorldEvent::Unmute);
    }},
  noSmoke{this, "no_smoke", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore,
    [this](bool value)
    {
      event(value ? WorldEvent::NoSmoke : WorldEvent::Smoke);
    }},
  simulation{this, "simulation", false, PropertyFlags::ReadWrite | PropertyFlags::NoStore,
    [this](bool value)
    {
      event(value ? WorldEvent::SimulationEnabled : WorldEvent::SimulationDisabled);
    }},
  save{*this, "save", MethodFlags::NoScript,
    [this]()
    {
      try
      {
        // backup world:
        const std::filesystem::path worldDir = Traintastic::instance->worldDir();
        const std::filesystem::path worldBackupDir = Traintastic::instance->worldBackupDir();
        auto dateTimeStr =
          []()
          {
            const auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
            std::stringstream ss;
            ss << std::put_time(std::localtime(&now), "_%Y%m%d_%H%M%S");
            return ss.str();
          };

        if(!std::filesystem::is_directory(worldBackupDir))
        {
          std::error_code ec;
          std::filesystem::create_directories(worldBackupDir, ec);
          if(ec)
            Log::log(*this, LogMessage::C1007_CREATING_WORLD_BACKUP_DIRECTORY_FAILED_X, ec);
        }

        if(std::filesystem::is_directory(worldDir / uuid.value()))
        {
          std::error_code ec;
          std::filesystem::rename(worldDir / uuid.value(), worldBackupDir / uuid.value() += dateTimeStr(), ec);
          if(ec)
            Log::log(*this, LogMessage::C1006_CREATING_WORLD_BACKUP_FAILED_X, ec);
        }

        if(std::filesystem::is_regular_file(worldDir / uuid.value() += dotCTW))
        {
          std::error_code ec;
          std::filesystem::rename(worldDir / uuid.value() += dotCTW, worldBackupDir / uuid.value() += dateTimeStr() += dotCTW, ec);
          if(ec)
            Log::log(*this, LogMessage::C1006_CREATING_WORLD_BACKUP_FAILED_X, ec);
        }

        // save world:
        std::filesystem::path savePath = worldDir / uuid.value();
        if(!Traintastic::instance->settings->saveWorldUncompressed)
          savePath += dotCTW;

        WorldSaver saver(*this, savePath);

        if(Traintastic::instance)
          Traintastic::instance->settings->lastWorld = uuid.value();

        Log::log(*this, LogMessage::N1022_SAVED_WORLD_X, name.value());
      }
      catch(const std::exception& e)
      {
        Log::log(*this, LogMessage::C1005_SAVING_WORLD_FAILED_X, e);
      }
    }}
  , getObject_{*this, "get_object", MethodFlags::Internal | MethodFlags::ScriptCallable,
      [this](const std::string& objectId)
      {
        return getObjectById(objectId);
      }}
  , getLNCVProgrammer{*this, "get_lncv_programmer", MethodFlags::NoScript,
      [](const ObjectPtr& interface) -> std::shared_ptr<LNCVProgrammer>
      {
        if(auto controller = std::dynamic_pointer_cast<LNCVProgrammingController>(interface))
          return std::make_shared<LNCVProgrammer>(*controller);
        return {};
      }}
  , onEvent{*this, "on_event", EventFlags::Scriptable}
{
  Attributes::addDisplayName(uuid, DisplayName::World::uuid);
  m_interfaceItems.add(uuid);
  Attributes::addDisplayName(name, DisplayName::Object::name);
  m_interfaceItems.add(name);
  Attributes::addEnabled(scale, false);
  Attributes::addValues(scale, WorldScaleValues);
  m_interfaceItems.add(scale);
  Attributes::addEnabled(scaleRatio, false);
  Attributes::addMinMax(scaleRatio, 1., 1000.);
  Attributes::addVisible(scaleRatio, false);
  m_interfaceItems.add(scaleRatio);

  Attributes::addObjectEditor(decoderControllers, false);
  m_interfaceItems.add(decoderControllers);
  Attributes::addObjectEditor(inputControllers, false);
  m_interfaceItems.add(inputControllers);
  Attributes::addObjectEditor(outputControllers, false);
  m_interfaceItems.add(outputControllers);
  Attributes::addObjectEditor(identificationControllers, false);
  m_interfaceItems.add(identificationControllers);
  Attributes::addObjectEditor(lncvProgrammingControllers, false);
  m_interfaceItems.add(lncvProgrammingControllers);

  Attributes::addObjectEditor(interfaces, false);
  m_interfaceItems.add(interfaces);
  Attributes::addObjectEditor(decoders, false);
  m_interfaceItems.add(decoders);
  Attributes::addObjectEditor(inputs, false);
  m_interfaceItems.add(inputs);
  Attributes::addObjectEditor(outputs, false);
  m_interfaceItems.add(outputs);
  Attributes::addObjectEditor(identifications, false);
  m_interfaceItems.add(identifications);
  Attributes::addObjectEditor(boards, false);
  m_interfaceItems.add(boards);
  Attributes::addObjectEditor(clock, false);
  m_interfaceItems.add(clock);
  Attributes::addObjectEditor(trains, false);
  m_interfaceItems.add(trains);
  Attributes::addObjectEditor(railVehicles, false);
  m_interfaceItems.add(railVehicles);
  Attributes::addObjectEditor(luaScripts, false);
  m_interfaceItems.add(luaScripts);

  Attributes::addObjectEditor(linkRailTiles, false);
  m_interfaceItems.add(linkRailTiles);

  Attributes::addObjectEditor(hardwareThrottles, false);
  m_interfaceItems.add(hardwareThrottles);

  Attributes::addObjectEditor(state, false);
  m_interfaceItems.add(state);
  Attributes::addObjectEditor(edit, false);
  m_interfaceItems.add(edit);
  Attributes::addObjectEditor(offline, false);
  m_interfaceItems.add(offline);
  Attributes::addObjectEditor(online, false);
  m_interfaceItems.add(online);
  Attributes::addObjectEditor(powerOff, false);
  m_interfaceItems.add(powerOff);
  Attributes::addObjectEditor(powerOn, false);
  m_interfaceItems.add(powerOn);
  Attributes::addObjectEditor(stop, false);
  m_interfaceItems.add(stop);
  Attributes::addObjectEditor(run, false);
  m_interfaceItems.add(run);
  Attributes::addObjectEditor(mute, false);
  m_interfaceItems.add(mute);
  Attributes::addObjectEditor(noSmoke, false);
  m_interfaceItems.add(noSmoke);
  Attributes::addEnabled(simulation, false);
  Attributes::addObjectEditor(simulation, false);
  m_interfaceItems.add(simulation);

  Attributes::addObjectEditor(save, false);
  m_interfaceItems.add(save);

  m_interfaceItems.add(getObject_);

  Attributes::addObjectEditor(getLNCVProgrammer, false);
  m_interfaceItems.add(getLNCVProgrammer);

  m_interfaceItems.add(onEvent);

  updateEnabled();
}

World::~World()
{
  removeAll(*interfaces);
  removeAll(*decoders);
  removeAll(*inputs);
  removeAll(*outputs);
  removeAll(*identifications);
  removeAll(*boards);
  removeAll(*trains);
  removeAll(*railVehicles);
  removeAll(*luaScripts);
}

std::string World::getUniqueId(std::string_view prefix) const
{
  std::string uniqueId{prefix};
  uniqueId.append("_");
  uint32_t number = 0;
  do
  {
    uniqueId.resize(prefix.size() + 1);
    uniqueId.append(std::to_string(++number));
  }
  while(isObject(uniqueId));

  return uniqueId;
}

bool World::isObject(const std::string& _id) const
{
  return m_objects.find(_id) != m_objects.end() || _id == id || _id == Traintastic::id;
}

ObjectPtr World::getObjectById(const std::string& _id) const
{
  auto it = m_objects.find(_id);
  if(it != m_objects.end())
    return it->second.lock();
  if(_id == classId)
    return std::const_pointer_cast<Object>(shared_from_this());
  return ObjectPtr();
}

ObjectPtr World::getObjectByPath(std::string_view path) const
{
  std::vector<std::string> ids;
  boost::split(ids, path, [](char c){ return c == '.'; });
  auto it = ids.cbegin();

  ObjectPtr obj = getObjectById(*it);
  while(obj && ++it != ids.cend())
  {
    if(AbstractProperty* property = obj->getProperty(*it); property && property->type() == ValueType::Object)
      obj = property->toObject();
    else if(AbstractVectorProperty* vectorProperty = obj->getVectorProperty(*it); vectorProperty && vectorProperty->type() == ValueType::Object)
    {
      obj.reset();
      const size_t size = vectorProperty->size();
      for(size_t i = 0; i < size; i++)
      {
        ObjectPtr v = vectorProperty->getObject(i);
        if(path == v->getObjectId())
        {
          obj = v;
          it++;
          break;
        }
      }
    }
    else
      obj.reset();
  }
  return obj;
}

bool World::export_(std::vector<std::byte>& data)
{
  try
  {
    WorldSaver saver(*this, data);
    Log::log(*this, LogMessage::N1025_EXPORTED_WORLD_SUCCESSFULLY);
    return true;
  }
  catch(const std::exception& e)
  {
    Log::log(*this, LogMessage::C1010_EXPORTING_WORLD_FAILED_X, e);
  }
  return false;
}

void World::loaded()
{
  updateScaleRatio();
  Object::loaded();
}

void World::worldEvent(WorldState worldState, WorldEvent worldEvent)
{
  Object::worldEvent(worldState, worldEvent);

  const bool editState = contains(worldState, WorldState::Edit);
  const bool runState = contains(worldState, WorldState::Run);

  Attributes::setEnabled(scale, editState && !runState);
  Attributes::setEnabled(scaleRatio, editState && !runState);

  fireEvent(onEvent, worldState, worldEvent);
}

void World::event(const WorldEvent value)
{
  // Update state:
  switch(value)
  {
    case WorldEvent::EditDisabled:
      state.setValueInternal(state.value() - WorldState::Edit);
      break;

    case WorldEvent::EditEnabled:
      state.setValueInternal(state.value() + WorldState::Edit);
      break;

    case WorldEvent::Offline:
      Log::log(*this, LogMessage::N1013_COMMUNICATION_DISABLED);
      state.setValueInternal(state.value() - WorldState::Online);
      break;

    case WorldEvent::Online:
      Log::log(*this, LogMessage::N1012_COMMUNICATION_ENABLED);
      state.setValueInternal(state.value() + WorldState::Online);
      break;

    case WorldEvent::PowerOff:
      Log::log(*this, LogMessage::N1015_POWER_OFF);
      state.setValueInternal(state.value() - WorldState::PowerOn - WorldState::Run);
      break;

    case WorldEvent::PowerOn:
      Log::log(*this, LogMessage::N1014_POWER_ON);
      state.setValueInternal(state.value() + WorldState::PowerOn);
      break;

    case WorldEvent::Stop:
      Log::log(*this, LogMessage::N1017_STOPPED);
      state.setValueInternal(state.value() - WorldState::Run);
      break;

    case WorldEvent::Run:
      Log::log(*this, LogMessage::N1016_RUNNING);
      state.setValueInternal(state.value() + WorldState::PowerOn + WorldState::Run);
      break;

    case WorldEvent::Unmute:
      Log::log(*this, LogMessage::N1019_MUTE_DISABLED);
      state.setValueInternal(state.value() - WorldState::Mute);
      break;

    case WorldEvent::Mute:
      Log::log(*this, LogMessage::N1018_MUTE_ENABLED);
      state.setValueInternal(state.value() + WorldState::Mute);
      break;

    case WorldEvent::NoSmoke:
      Log::log(*this, LogMessage::N1021_SMOKE_DISABLED);
      state.setValueInternal(state.value() + WorldState::NoSmoke);
      break;

    case WorldEvent::Smoke:
      Log::log(*this, LogMessage::N1020_SMOKE_ENABLED);
      state.setValueInternal(state.value() - WorldState::NoSmoke);
      break;

    case WorldEvent::SimulationDisabled:
      Log::log(*this, LogMessage::N1023_SIMULATION_DISABLED);
      state.setValueInternal(state.value() - WorldState::Simulation);
      break;

    case WorldEvent::SimulationEnabled:
      Log::log(*this, LogMessage::N1024_SIMULATION_ENABLED);
      state.setValueInternal(state.value() + WorldState::Simulation);
      break;
  }

  updateEnabled();

  const WorldState worldState = state;
  worldEvent(worldState, value);
  for(auto& it : m_objects)
    it.second.lock()->worldEvent(worldState, value);
}

void World::updateEnabled()
{
  const bool isOnline = contains(state.value(), WorldState::Online);
  const bool isPoweredOn = contains(state.value(), WorldState::PowerOn);
  const bool isRunning = contains(state.value(), WorldState::Run);

  Attributes::setEnabled(simulation, !isOnline && !isPoweredOn && !isRunning);
}

void World::updateScaleRatio()
{
  if(scale != WorldScale::Custom)
  {
    scaleRatio.setValueInternal(getScaleRatio(scale));
    Attributes::setVisible(scaleRatio, false);
  }
  else
    Attributes::setVisible(scaleRatio, true);
}
