#include "h7992_minimal_driver.hpp"

#include <array>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <linux/gpio.h>
#include <stdexcept>
#include <sys/ioctl.h>
#include <unistd.h>
#include <utility>

namespace radar::drivers {
namespace {

// Position mapping for H7992 control pins:
// position -> (A, B)
constexpr std::array<std::array<std::uint8_t, 2>, 4> kPositionToAB = {
    std::array<std::uint8_t, 2>{0, 0},
    std::array<std::uint8_t, 2>{0, 1},
    std::array<std::uint8_t, 2>{1, 0},
    std::array<std::uint8_t, 2>{1, 1},
};

void validate_open_settings(const H7992MinimalDriverSettings& settings) {
    if (settings.positions == 0U || settings.positions > 4U) {
        throw std::runtime_error("H7992 switch positions must be in range [1, 4] for " + settings.name);
    }
    if (settings.default_position >= settings.positions) {
        throw std::runtime_error("Switch default_position out of range for " + settings.name);
    }
}

[[nodiscard]] auto make_line_values(std::uint8_t pin_a_state, std::uint8_t pin_b_state) -> gpio_v2_line_values {
    gpio_v2_line_values values{};
    values.mask = (1ULL << 0U) | (1ULL << 1U);
    values.bits =
        (static_cast<std::uint64_t>(pin_a_state) << 0U) | (static_cast<std::uint64_t>(pin_b_state) << 1U);
    return values;
}

}  // namespace

H7992MinimalDriver::H7992MinimalDriver(H7992MinimalDriverSettings settings) : settings_(std::move(settings)) {}

void H7992MinimalDriver::open() {
    if (is_open_) {
        return;
    }
    validate_open_settings(settings_);

    if (settings_.mode == config::DriverMode::Native) {
        open_native();
    }

    is_open_ = true;
    switch_to(settings_.default_position);
}

void H7992MinimalDriver::close() {
    if (!is_open_) {
        return;
    }

    if (settings_.mode == config::DriverMode::Native) {
        close_native();
    }

    is_open_ = false;
}

auto H7992MinimalDriver::position_count() const -> std::uint32_t {
    return settings_.positions;
}

void H7992MinimalDriver::switch_to(std::uint32_t position) {
    if (!is_open_) {
        throw std::runtime_error("Switch driver is not open for " + settings_.name);
    }
    if (position >= settings_.positions) {
        throw std::runtime_error("Switch position out of range for " + settings_.name);
    }

    switch (settings_.mode) {
        case config::DriverMode::Mock:
            break;
        case config::DriverMode::Native:
            switch_native(position);
            break;
        default:
            throw std::runtime_error("Unsupported switch driver mode for " + settings_.name);
    }

    current_position_ = position;
}

void H7992MinimalDriver::open_native() {
    if (settings_.gpio_chip.empty()) {
        throw std::runtime_error("gpio_chip is empty for native switch " + settings_.name);
    }
    if (settings_.pin_a < 0 || settings_.pin_b < 0 || settings_.pin_a == settings_.pin_b) {
        throw std::runtime_error("Invalid pin_a/pin_b for native switch " + settings_.name);
    }

    chip_fd_ = ::open(settings_.gpio_chip.c_str(), O_RDONLY | O_CLOEXEC);
    if (chip_fd_ < 0) {
        throw std::runtime_error(
            "Failed to open gpio chip '" + settings_.gpio_chip + "' for " + settings_.name + ": " + std::strerror(errno)
        );
    }

    gpio_v2_line_request request{};
    // The first requested line maps to logical bit 0, the second to bit 1.
    request.offsets[0] = static_cast<std::uint32_t>(settings_.pin_a);
    request.offsets[1] = static_cast<std::uint32_t>(settings_.pin_b);
    request.num_lines = 2;
    request.config.flags = GPIO_V2_LINE_FLAG_OUTPUT;
    std::snprintf(request.consumer, sizeof(request.consumer), "radar_%s", settings_.name.c_str());

    if (::ioctl(chip_fd_, GPIO_V2_GET_LINE_IOCTL, &request) != 0) {
        const auto error = std::strerror(errno);
        ::close(chip_fd_);
        chip_fd_ = -1;
        throw std::runtime_error("Failed to request GPIO lines for " + settings_.name + ": " + error);
    }

    line_fd_ = request.fd;
    if (line_fd_ < 0) {
        ::close(chip_fd_);
        chip_fd_ = -1;
        throw std::runtime_error("GPIO line request returned invalid fd for " + settings_.name);
    }
}

void H7992MinimalDriver::close_native() {
    if (line_fd_ >= 0) {
        ::close(line_fd_);
        line_fd_ = -1;
    }
    if (chip_fd_ >= 0) {
        ::close(chip_fd_);
        chip_fd_ = -1;
    }
}

void H7992MinimalDriver::switch_native(std::uint32_t position) {
    if (line_fd_ < 0) {
        throw std::runtime_error("Native GPIO line fd is not open for " + settings_.name);
    }

    const auto [pin_a_state, pin_b_state] = kPositionToAB[position];
    auto values = make_line_values(pin_a_state, pin_b_state);

    if (::ioctl(line_fd_, GPIO_V2_LINE_SET_VALUES_IOCTL, &values) != 0) {
        throw std::runtime_error("Failed to switch GPIO state for " + settings_.name + ": " + std::strerror(errno));
    }
}

}  // namespace radar::drivers
