/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "usb_libusb_inhouse_hotplug.h"

#include <chrono>
#include <thread>
#include <unordered_map>

#include "adb_trace.h"
#include "client/usb_libusb_hotplug.h"

#ifdef ANDROID_TOOLS_USE_BUNDLED_LIBUSB
#include "libusb/libusb.h"
#else
#include <libusb-1.0/libusb.h>
#endif

namespace libusb_inhouse_hotplug {

class ScanRateLimiter {
  public:
    ScanRateLimiter(std::chrono::seconds rate) : rate_s_(rate) { Tick(); }
    bool Exceeded() {
        auto elapsed_since_last_scan = std::chrono::duration_cast<std::chrono::seconds>(
                std::chrono::steady_clock::now() - last_tick_);
        return elapsed_since_last_scan < rate_s_;
    }

    void Tick() { last_tick_ = std::chrono::steady_clock::now(); }

  private:
    std::chrono::seconds rate_s_;
    std::chrono::time_point<std::chrono::steady_clock> last_tick_;
};

std::chrono::seconds kScan_rate_s = std::chrono::seconds(2);
static ScanRateLimiter rate_limiter{kScan_rate_s};

// We need to synchronize access to the list of known devices. It can be modified from both the
// monitoring thread but also LibUsbConnection threads (when they report being closed).
static std::mutex known_devices_mutex [[clang::no_destroy]];
static std::unordered_map<uint64_t, libusb_device*> GUARDED_BY(known_devices_mutex) known_devices
        [[clang::no_destroy]];

void scan() {
    if (rate_limiter.Exceeded()) {
        return;
    }
    rate_limiter.Tick();

    VLOG(USB) << "inhouse USB scanning";
    std::lock_guard<std::mutex> lock(known_devices_mutex);

    // First retrieve all connected devices and detect new ones.
    libusb_device** devs = nullptr;
    libusb_get_device_list(nullptr, &devs);
    std::unordered_map<uint64_t, libusb_device*> current_devices;
    for (size_t i = 0; devs[i] != nullptr; i++) {
        libusb_device* dev = devs[i];
        auto session_id = LibUsbDevice::GenerateSessionId(dev).id;
        if (!known_devices.contains(session_id) && !current_devices.contains(session_id)) {
            hotplug_callback(nullptr, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, nullptr);
        }
        current_devices[session_id] = dev;
    }

    // Handle disconnected devices
    for (const auto& [session_id, dev] : known_devices) {
        if (!current_devices.contains(session_id)) {
            hotplug_callback(nullptr, dev, LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, nullptr);
        }
    }
    known_devices = std::move(current_devices);
    libusb_free_device_list(devs, false);
}

void report_error(const LibUsbConnection& connection) {
    if (libusb_has_capability(LIBUSB_CAP_HAS_HOTPLUG)) {
        return;
    }
    std::lock_guard<std::mutex> lock(known_devices_mutex);
    known_devices.erase(connection.GetSessionId());
}
}  // end namespace libusb_inhouse_hotplug