#include "shell/overview/overview.h"

#include "config/config_service.h"
#include "core/log.h"
#include "render/core/shared_texture_cache.h"
#include "shell/overview/overview_surface.h"
#include "ui/palette.h"
#include "wayland/wayland_connection.h"

#include <algorithm>

namespace {

  constexpr Logger kLog("overview");
  constexpr auto kDestroyDelay = std::chrono::milliseconds(2000);

} // namespace

Overview::Overview() = default;
Overview::~Overview() = default;

void Overview::destroyInstances() {
  for (auto& inst : m_instances) {
    releaseInstanceTexture(*inst);
  }
  m_instances.clear();
}

bool Overview::initialize(WaylandConnection& wayland, ConfigService* config, SharedTextureCache* textureCache,
                          GlSharedContext* sharedGl) {
  m_wayland = &wayland;
  m_config = config;
  m_textureCache = textureCache;
  m_sharedGl = sharedGl;

  // Register reload callback unconditionally so toggling enabled in config works.
  m_config->addReloadCallback([this]() { reload(); });

  if (!m_config->config().overview.enabled) {
    kLog.info("disabled in config");
    return true;
  }

  if (m_active) {
    syncInstances();
  }
  return true;
}

void Overview::reload() {
  kLog.info("reloading config");
  m_destroyDelayTimer.stop();

  // Always tear down existing instances. This is necessary because a
  // wallpaper enable/disable cycle resets the wallpaper share context, and any
  // overview instances created against the old context cannot access the new
  // textures. Full teardown + recreate is safe since overview surfaces are
  // hidden by the compositor outside of overview mode (no visible flash).
  destroyInstances();

  if (!m_config->config().overview.enabled) {
    return;
  }

  if (m_active) {
    syncInstances();
  }
}

void Overview::onOutputChange() {
  if (m_config == nullptr || !m_config->config().overview.enabled || !m_active) {
    return;
  }
  syncInstances();
}

void Overview::onStateChange() {
  kLog.info("state changed, checking wallpaper updates");

  for (auto& inst : m_instances) {
    auto newPath = m_config->getWallpaperPath(inst->connectorName);
    if (newPath.empty() || newPath == inst->currentPath) {
      continue;
    }

    kLog.info("updating {} → {}", inst->connectorName, newPath);
    if (!m_active) {
      releaseInstanceTexture(*inst, false);
      inst->currentPath = newPath;
      continue;
    }
    releaseInstanceTexture(*inst);
    loadWallpaper(*inst, newPath);
  }
}

void Overview::onThemeChanged() {
  if (!m_active) {
    return;
  }
  for (auto& inst : m_instances) {
    updateRendererState(*inst);
    if (inst->surface != nullptr) {
      inst->surface->requestRedraw();
    }
  }
}

void Overview::onFontChanged() { requestLayout(); }

void Overview::requestLayout() {
  for (auto& inst : m_instances) {
    if (inst->surface != nullptr) {
      inst->surface->requestLayout();
    }
  }
}

void Overview::setActive(bool active) {
  const bool overviewEnabled = (m_config != nullptr) ? m_config->config().overview.enabled : false;
  if (!overviewEnabled) {
    m_destroyDelayTimer.stop();
    if (!m_instances.empty()) {
      destroyInstances();
    }
    m_active = false;
    return;
  }

  if (m_active == active) {
    return;
  }
  m_active = active;
  const bool unloadWhenNotInUse = (m_config != nullptr) ? m_config->config().overview.unloadWhenNotInUse : true;

  if (!m_active) {
    for (auto& inst : m_instances) {
      if (inst->surface != nullptr) {
        inst->surface->setActive(false);
      }
    }
    if (unloadWhenNotInUse) {
      m_destroyDelayTimer.start(kDestroyDelay, [this]() {
        if (!m_active) {
          destroyInstances();
        }
      });
    } else {
      m_destroyDelayTimer.stop();
    }
    return;
  }

  m_destroyDelayTimer.stop();
  if (m_instances.empty()) {
    syncInstances();
  }
  for (auto& inst : m_instances) {
    if (inst->surface == nullptr) {
      continue;
    }
    inst->surface->setActive(true);

    if (inst->currentTexture.id == 0) {
      std::string path = inst->currentPath;
      if (path.empty() && m_config != nullptr) {
        path = m_config->getWallpaperPath(inst->connectorName);
      }
      if (!path.empty()) {
        loadWallpaper(*inst, path);
      }
    } else {
      inst->surface->requestRedraw();
    }
  }
}

void Overview::syncInstances() {
  const auto& outputs = m_wayland->outputs();

  // Remove instances for outputs that no longer exist
  std::erase_if(m_instances, [&](const auto& inst) {
    bool found =
        std::any_of(outputs.begin(), outputs.end(), [&inst](const auto& out) { return out.name == inst->outputName; });
    if (!found) {
      kLog.info("removing instance for output {}", inst->outputName);
      releaseInstanceTexture(*inst);
    }
    return !found;
  });

  // Create instances for new outputs
  for (const auto& output : outputs) {
    if (!output.done || output.connectorName.empty()) {
      continue;
    }

    bool exists = std::any_of(m_instances.begin(), m_instances.end(),
                              [&output](const auto& inst) { return inst->outputName == output.name; });
    if (!exists) {
      createInstance(output);
    }
  }
}

void Overview::createInstance(const WaylandOutput& output) {
  auto wallpaperPath = m_config->getWallpaperPath(output.connectorName);
  kLog.info("creating on {} ({}), path={}", output.connectorName, output.description, wallpaperPath);

  auto inst = std::make_unique<OverviewInstance>();
  inst->outputName = output.name;
  inst->output = output.output;
  inst->scale = output.scale;
  inst->connectorName = output.connectorName;

  auto surfaceConfig = LayerSurfaceConfig{
      .nameSpace = "noctalia-overview",
      .layer = LayerShellLayer::Background,
      .anchor = LayerShellAnchor::Top | LayerShellAnchor::Bottom | LayerShellAnchor::Left | LayerShellAnchor::Right,
      .width = 0,
      .height = 0,
      .exclusiveZone = -1,
  };

  inst->surface = std::make_unique<OverviewSurface>(*m_wayland, std::move(surfaceConfig));
  inst->surface->setSharedGl(m_sharedGl);

  updateRendererState(*inst);

  auto* instPtr = inst.get();
  inst->surface->setConfigureCallback([this, instPtr](std::uint32_t /*width*/, std::uint32_t /*height*/) {
    if (instPtr->currentTexture.id != 0 || !m_active || m_config == nullptr) {
      return;
    }
    std::string path = instPtr->currentPath;
    if (path.empty()) {
      path = m_config->getWallpaperPath(instPtr->connectorName);
    }
    if (!path.empty()) {
      loadWallpaper(*instPtr, path);
    }
  });

  if (!inst->surface->initialize(output.output)) {
    kLog.warn("failed to initialize overview surface for output {}", output.name);
    return;
  }

  inst->surface->setActive(m_active);

  m_instances.push_back(std::move(inst));
}

void Overview::loadWallpaper(OverviewInstance& inst, const std::string& path) {
  auto tex = m_textureCache->acquire(path);
  if (tex.id == 0) {
    kLog.warn("failed to load {}", path);
    return;
  }

  inst.currentTexture = tex;
  inst.currentPath = path;
  updateRendererState(inst);
  if (m_active && inst.surface != nullptr) {
    inst.surface->requestRedraw();
  }
}

void Overview::updateRendererState(OverviewInstance& inst) {
  if (inst.surface == nullptr) {
    return;
  }

  const auto& ov = m_config->config().overview;
  inst.surface->setUnloadWhenInactive(ov.unloadWhenNotInUse);
  inst.surface->setBlurIntensity(ov.blurIntensity);
  inst.surface->setTintIntensity(ov.tintIntensity);

  // Tint color from the current surface role.
  const Color surface = resolveThemeColor(roleColor(ColorRole::Surface));
  inst.surface->setTintColor(surface.r, surface.g, surface.b);

  if (inst.currentTexture.id != 0) {
    inst.surface->setWallpaperState(inst.currentTexture.id, static_cast<float>(inst.currentTexture.width),
                                    static_cast<float>(inst.currentTexture.height),
                                    m_config->config().wallpaper.fillMode);
  } else {
    inst.surface->setWallpaperState(0, 0.0f, 0.0f, m_config->config().wallpaper.fillMode);
  }
}

void Overview::releaseInstanceTexture(OverviewInstance& inst, bool clearPath) {
  if (inst.currentTexture.id != 0) {
    m_textureCache->release(inst.currentTexture, inst.currentPath);
    inst.currentTexture = {};
  }
  if (clearPath) {
    inst.currentPath.clear();
  }
}
