#include "camera_manager.hpp"

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <set>
#include <sstream>

using json = nlohmann::json;
namespace fs = std::filesystem;

CameraManager::CameraManager(int width, int height, int fps)
    : width_(width), height_(height), fps_(std::max(1, fps))
{
}

CameraManager::~CameraManager()
{
    stop();
}

bool CameraManager::isVideoFile(const std::string &source)
{
    std::string lower = source;
    std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char ch)
                   { return static_cast<char>(std::tolower(ch)); });
    for (const std::string &extension : {".mp4", ".avi", ".mkv", ".mov", ".webm"})
    {
        if (lower.size() >= extension.size() && lower.compare(lower.size() - extension.size(), extension.size(), extension) == 0)
            return true;
    }
    return false;
}

cv::Mat CameraManager::applyDigitalView(const cv::Mat &input, const View &view, int output_width, int output_height)
{
    if (input.empty())
        return {};

    const double zoom = std::clamp(view.zoom, 1.0, 4.0);
    const int crop_width = std::max(2, static_cast<int>(input.cols / zoom));
    const int crop_height = std::max(2, static_cast<int>(input.rows / zoom));
    const int max_x = std::max(0, input.cols - crop_width);
    const int max_y = std::max(0, input.rows - crop_height);

    const double normalized_x = (std::clamp(view.pan_x, -1.0, 1.0) + 1.0) * 0.5;
    const double normalized_y = (std::clamp(view.pan_y, -1.0, 1.0) + 1.0) * 0.5;
    const int x = std::clamp(static_cast<int>(normalized_x * max_x), 0, max_x);
    const int y = std::clamp(static_cast<int>(normalized_y * max_y), 0, max_y);

    cv::Mat cropped = input(cv::Rect(x, y, crop_width, crop_height));
    cv::Mat output;
    cv::resize(cropped, output, cv::Size(output_width, output_height), 0.0, 0.0, cv::INTER_LINEAR);
    return output;
}

void CameraManager::stopCapture()
{
    running_ = false;
    if (capture_thread_.joinable())
        capture_thread_.join();
}

void CameraManager::stop()
{
    stopCapture();
    for (auto &capture : captures_)
        capture.release();

    std::lock_guard<std::mutex> lock(mutex_);
    online_.fill(false);
}

bool CameraManager::configureSources(const std::array<std::string, CameraCount> &sources, std::string &error)
{
    std::set<std::string> unique_sources;
    for (const auto &source : sources)
    {
        if (!source.empty() && !unique_sources.insert(source).second)
        {
            error = "Samma kamerakälla kan inte användas två gånger: " + source;
            return false;
        }
    }

    stopCapture();
    for (auto &capture : captures_)
        capture.release();

    {
        std::lock_guard<std::mutex> lock(mutex_);
        sources_ = sources;
        online_.fill(false);
        errors_.fill({});
        sequences_.fill(0);
        for (auto &frame : latest_frames_)
            frame.release();
    }

    int opened = 0;
    std::ostringstream failures;
    for (std::size_t index = 0; index < CameraCount; ++index)
    {
        if (sources[index].empty())
        {
            std::lock_guard<std::mutex> lock(mutex_);
            errors_[index] = "Ingen källa vald";
            failures << "Kamera " << index + 1 << ": ingen källa vald. ";
            continue;
        }

        bool success = false;
        if (isVideoFile(sources[index]))
        {
            success = captures_[index].open(sources[index]);
            if (success)
            {
                // The bundled recordings contain a stable, empty board a few
                // seconds in. Match the original detector's mock-camera mode.
                const double source_fps = captures_[index].get(cv::CAP_PROP_FPS);
                const double seek_seconds = 3.0 - static_cast<double>(index) * 0.18;
                if (source_fps > 0.0)
                    captures_[index].set(cv::CAP_PROP_POS_FRAMES, source_fps * seek_seconds);
            }
        }
        else
        {
            success = captures_[index].open(sources[index], cv::CAP_V4L2);
            if (success)
            {
                captures_[index].set(cv::CAP_PROP_FOURCC, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'));
                captures_[index].set(cv::CAP_PROP_FRAME_WIDTH, width_);
                captures_[index].set(cv::CAP_PROP_FRAME_HEIGHT, height_);
                captures_[index].set(cv::CAP_PROP_FPS, fps_);
                captures_[index].set(cv::CAP_PROP_BUFFERSIZE, 1);
            }
        }

        {
            std::lock_guard<std::mutex> lock(mutex_);
            online_[index] = success;
            if (!success)
            {
                errors_[index] = "Kunde inte öppna " + sources[index];
                failures << "Kamera " << index + 1 << ": kunde inte öppna " << sources[index] << ". ";
            }
        }
        if (success)
            ++opened;
    }

    if (opened > 0)
        startCapture();

    error = failures.str();
    return opened == static_cast<int>(CameraCount);
}

bool CameraManager::updateView(std::size_t index, const View &view, std::string &error)
{
    if (index >= CameraCount)
    {
        error = "Ogiltigt kameraindex";
        return false;
    }

    if (view.zoom < 1.0 || view.zoom > 4.0 || view.pan_x < -1.0 || view.pan_x > 1.0 || view.pan_y < -1.0 || view.pan_y > 1.0)
    {
        error = "Zoom måste vara 1–4 och panorering -1–1";
        return false;
    }

    std::lock_guard<std::mutex> lock(mutex_);
    views_[index] = view;
    return true;
}

void CameraManager::startCapture()
{
    if (running_)
        return;
    running_ = true;
    capture_thread_ = std::thread(&CameraManager::captureLoop, this);
}

void CameraManager::captureLoop()
{
    const auto frame_interval = std::chrono::milliseconds(std::max(1, 1000 / fps_));

    while (running_)
    {
        const auto started = std::chrono::steady_clock::now();

        for (std::size_t index = 0; index < CameraCount && running_; ++index)
        {
            if (!captures_[index].isOpened())
                continue;

            cv::Mat frame;
            bool read = captures_[index].read(frame);
            if ((!read || frame.empty()) && isVideoFile(sources_[index]))
            {
                captures_[index].set(cv::CAP_PROP_POS_FRAMES, 0);
                read = captures_[index].read(frame);
            }

            std::lock_guard<std::mutex> lock(mutex_);
            if (read && !frame.empty())
            {
                latest_frames_[index] = frame.clone();
                online_[index] = true;
                errors_[index].clear();
                ++sequences_[index];
            }
            else
            {
                online_[index] = false;
                errors_[index] = "Ingen bild från kameran";
            }
        }

        const auto elapsed = std::chrono::steady_clock::now() - started;
        if (elapsed < frame_interval)
            std::this_thread::sleep_for(frame_interval - elapsed);
    }
}

cv::Mat CameraManager::frame(std::size_t index, bool apply_view) const
{
    if (index >= CameraCount)
        return {};

    cv::Mat raw;
    View view;
    {
        std::lock_guard<std::mutex> lock(mutex_);
        if (latest_frames_[index].empty())
            return {};
        raw = latest_frames_[index].clone();
        view = views_[index];
    }

    if (!apply_view)
        return raw;
    return applyDigitalView(raw, view, width_, height_);
}

std::vector<cv::Mat> CameraManager::frames(bool apply_view) const
{
    std::vector<cv::Mat> result;
    result.reserve(CameraCount);
    for (std::size_t index = 0; index < CameraCount; ++index)
        result.push_back(frame(index, apply_view));
    return result;
}

std::vector<cv::Mat> CameraManager::averageFrames(int samples, int interval_ms, std::string &error) const
{
    samples = std::clamp(samples, 1, 120);
    std::array<cv::Mat, CameraCount> sums;
    int accepted = 0;

    for (int sample = 0; sample < samples; ++sample)
    {
        const auto current = frames(true);
        const bool complete = current.size() == CameraCount &&
                              std::all_of(current.begin(), current.end(), [](const cv::Mat &value)
                                          { return !value.empty(); });
        if (complete)
        {
            for (std::size_t index = 0; index < CameraCount; ++index)
            {
                cv::Mat converted;
                current[index].convertTo(converted, CV_32FC3);
                if (sums[index].empty())
                    sums[index] = converted;
                else
                    sums[index] += converted;
            }
            ++accepted;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(std::max(0, interval_ms)));
    }

    if (accepted == 0)
    {
        error = "Kunde inte hämta kompletta bilder från alla tre kameror";
        return {};
    }

    std::vector<cv::Mat> averages;
    averages.reserve(CameraCount);
    for (auto &sum : sums)
    {
        cv::Mat output;
        sum.convertTo(output, CV_8UC3, 1.0 / accepted);
        averages.push_back(output);
    }
    return averages;
}

bool CameraManager::ready() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    for (std::size_t index = 0; index < CameraCount; ++index)
    {
        if (!online_[index] || latest_frames_[index].empty())
            return false;
    }
    return true;
}

std::array<std::string, CameraManager::CameraCount> CameraManager::sources() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    return sources_;
}

json CameraManager::status() const
{
    std::lock_guard<std::mutex> lock(mutex_);
    json cameras = json::array();
    for (std::size_t index = 0; index < CameraCount; ++index)
    {
        cameras.push_back({
            {"index", index},
            {"source", sources_[index]},
            {"online", online_[index] && !latest_frames_[index].empty()},
            {"error", errors_[index]},
            {"sequence", sequences_[index]},
            {"frame_width", latest_frames_[index].empty() ? 0 : latest_frames_[index].cols},
            {"frame_height", latest_frames_[index].empty() ? 0 : latest_frames_[index].rows},
            {"view", {
                 {"zoom", views_[index].zoom},
                 {"pan_x", views_[index].pan_x},
                 {"pan_y", views_[index].pan_y},
             }},
        });
    }

    return {
        {"ready", std::all_of(cameras.begin(), cameras.end(), [](const json &camera)
                              { return camera.value("online", false); })},
        {"width", width_},
        {"height", height_},
        {"fps", fps_},
        {"cameras", cameras},
    };
}

std::string CameraManager::deviceLabel(const std::string &device_path)
{
    const std::string name = fs::path(device_path).filename().string();
    std::ifstream input("/sys/class/video4linux/" + name + "/name");
    std::string label;
    std::getline(input, label);
    if (label.empty())
        label = name;
    return label;
}

json CameraManager::availableSources() const
{
    json result = json::array();
    std::set<std::string> resolved_devices;

    const fs::path by_id("/dev/v4l/by-id");
    std::error_code error;
    if (fs::exists(by_id, error))
    {
        std::vector<fs::path> paths;
        for (const auto &entry : fs::directory_iterator(by_id, error))
            paths.push_back(entry.path());
        std::sort(paths.begin(), paths.end());

        for (const auto &path : paths)
        {
            const fs::path canonical = fs::weakly_canonical(path, error);
            if (error)
                continue;
            resolved_devices.insert(canonical.string());
            result.push_back({{"source", path.string()}, {"label", path.filename().string()}, {"type", "camera"}});
        }
    }

    std::vector<fs::path> devices;
    for (int index = 0; index < 64; ++index)
    {
        fs::path path("/dev/video" + std::to_string(index));
        if (fs::exists(path, error))
            devices.push_back(path);
    }
    for (const auto &path : devices)
    {
        const fs::path canonical = fs::weakly_canonical(path, error);
        if (error || resolved_devices.count(canonical.string()) > 0)
            continue;
        result.push_back({{"source", path.string()}, {"label", deviceLabel(path.string()) + " (" + path.filename().string() + ")"}, {"type", "camera"}});
    }

    std::lock_guard<std::mutex> lock(mutex_);
    for (const auto &source : sources_)
    {
        if (!source.empty() && isVideoFile(source))
        {
            const bool already_listed = std::any_of(result.begin(), result.end(), [&](const json &item)
                                                    { return item.value("source", "") == source; });
            if (!already_listed)
                result.push_back({{"source", source}, {"label", fs::path(source).filename().string() + " (testvideo)"}, {"type", "file"}});
        }
    }
    return result;
}
