/*
 * Copyright 2007-2008, Haiku Inc. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *		Michael Lotz <mmlr@mlotz.ch>
 */

#include "haiku_usb.h"
#include <cstdio>
#include <Directory.h>
#include <Entry.h>
#include <Looper.h>
#include <Messenger.h>
#include <Node.h>
#include <NodeMonitor.h>
#include <Path.h>
#include <cstring>

class WatchedEntry {
public:
			WatchedEntry(BMessenger *, entry_ref *);
			~WatchedEntry();
	bool		EntryCreated(entry_ref *ref);
	bool		EntryRemoved(ino_t node);
	bool		InitCheck();

private:
	BMessenger*	fMessenger;
	node_ref	fNode;
	bool		fIsDirectory;
	USBDevice*	fDevice;
	WatchedEntry*	fEntries;
	WatchedEntry*	fLink;
	bool		fInitCheck;
};


class RosterLooper : public BLooper {
public:
			RosterLooper(USBRoster *);
	void		Stop();
	virtual void	MessageReceived(BMessage *);
	bool		InitCheck();

private:
	USBRoster*	fRoster;
	WatchedEntry*	fRoot;
	BMessenger*	fMessenger;
	bool		fInitCheck;
};


WatchedEntry::WatchedEntry(BMessenger *messenger, entry_ref *ref)
	:	fMessenger(messenger),
		fIsDirectory(false),
		fDevice(NULL),
		fEntries(NULL),
		fLink(NULL),
		fInitCheck(false)
{
	BEntry entry(ref);
	entry.GetNodeRef(&fNode);

	BDirectory directory;
	if (entry.IsDirectory() && directory.SetTo(ref) >= B_OK) {
		fIsDirectory = true;

		while (directory.GetNextEntry(&entry) >= B_OK) {
			if (entry.GetRef(ref) < B_OK)
				continue;

			WatchedEntry *child = new(std::nothrow) WatchedEntry(fMessenger, ref);
			if (child == NULL)
				continue;
			if (child->InitCheck() == false) {
				delete child;
				continue;
			}

			child->fLink = fEntries;
			fEntries = child;
		}

		watch_node(&fNode, B_WATCH_DIRECTORY, *fMessenger);
	}
	else {
		if (strncmp(ref->name, "raw", 3) == 0)
			return;

		BPath path, parent_path;
		entry.GetPath(&path);
		fDevice = new(std::nothrow) USBDevice(path.Path());
		if (fDevice != NULL && fDevice->InitCheck() == true) {
			// Add this new device to each active context's device list
			struct libusb_context *ctx;
			unsigned long session_id = (unsigned long)&fDevice;

			usbi_mutex_lock(&active_contexts_lock);
			for_each_context(ctx) {
				struct libusb_device *dev = usbi_get_device_by_session_id(ctx, session_id);
				if (dev) {
					usbi_dbg(NULL, "using previously allocated device with location %lu", session_id);
					libusb_unref_device(dev);
					continue;
				}
				usbi_dbg(NULL, "allocating new device with location %lu", session_id);
				dev = usbi_alloc_device(ctx, session_id);
				if (!dev) {
					usbi_dbg(NULL, "device allocation failed");
					continue;
				}
				*((USBDevice **)usbi_get_device_priv(dev)) = fDevice;

				// Calculate pseudo-device-address
				int addr, tmp;
				if (strcmp(path.Leaf(), "hub") == 0)
					tmp = 100;	//Random Number
				else
					sscanf(path.Leaf(), "%d", &tmp);
				addr = tmp + 1;
				path.GetParent(&parent_path);
				while (strcmp(parent_path.Leaf(), "usb") != 0) {
					sscanf(parent_path.Leaf(), "%d", &tmp);
					addr += tmp + 1;
					parent_path.GetParent(&parent_path);
				}
				sscanf(path.Path(), "/dev/bus/usb/%hhu", &dev->bus_number);
				dev->device_address = addr - (dev->bus_number + 1);

				static_assert(sizeof(dev->device_descriptor) == sizeof(usb_device_descriptor),
					      "mismatch between libusb and OS device descriptor sizes");
				memcpy(&dev->device_descriptor, fDevice->Descriptor(), LIBUSB_DT_DEVICE_SIZE);
				usbi_localize_device_descriptor(&dev->device_descriptor);

				if (usbi_sanitize_device(dev) < 0) {
					usbi_dbg(NULL, "device sanitization failed");
					libusb_unref_device(dev);
					continue;
				}
				usbi_connect_device(dev);
			}
			usbi_mutex_unlock(&active_contexts_lock);
		}
		else if (fDevice) {
			delete fDevice;
			fDevice = NULL;
			return;
		}
	}
	fInitCheck = true;
}


WatchedEntry::~WatchedEntry()
{
	if (fIsDirectory) {
		watch_node(&fNode, B_STOP_WATCHING, *fMessenger);

		WatchedEntry *child = fEntries;
		while (child) {
			WatchedEntry *next = child->fLink;
			delete child;
			child = next;
		}
	}

	if (fDevice) {
		// Remove this device from each active context's device list
		struct libusb_context *ctx;
		struct libusb_device *dev;
		unsigned long session_id = (unsigned long)&fDevice;

		usbi_mutex_lock(&active_contexts_lock);
		for_each_context(ctx) {
			dev = usbi_get_device_by_session_id(ctx, session_id);
			if (dev != NULL) {
				usbi_disconnect_device(dev);
				libusb_unref_device(dev);
			} else {
				usbi_dbg(ctx, "device with location %lu not found", session_id);
			}
		}
		usbi_mutex_static_unlock(&active_contexts_lock);
		delete fDevice;
	}
}


bool
WatchedEntry::EntryCreated(entry_ref *ref)
{
	if (!fIsDirectory)
		return false;

	if (ref->directory != fNode.node) {
		WatchedEntry *child = fEntries;
		while (child) {
			if (child->EntryCreated(ref))
				return true;
			child = child->fLink;
		}
		return false;
	}

	WatchedEntry *child = new(std::nothrow) WatchedEntry(fMessenger, ref);
	if (child == NULL)
		return false;
	child->fLink = fEntries;
	fEntries = child;
	return true;
}


bool
WatchedEntry::EntryRemoved(ino_t node)
{
	if (!fIsDirectory)
		return false;

	WatchedEntry *child = fEntries;
	WatchedEntry *lastChild = NULL;
	while (child) {
		if (child->fNode.node == node) {
			if (lastChild)
				lastChild->fLink = child->fLink;
			else
				fEntries = child->fLink;
			delete child;
			return true;
		}

		if (child->EntryRemoved(node))
			return true;

		lastChild = child;
		child = child->fLink;
	}
	return false;
}


bool
WatchedEntry::InitCheck()
{
	return fInitCheck;
}


RosterLooper::RosterLooper(USBRoster *roster)
	:	BLooper("LibusbRoster Looper"),
		fRoster(roster),
		fRoot(NULL),
		fMessenger(NULL),
		fInitCheck(false)
{
	BEntry entry("/dev/bus/usb");
	if (!entry.Exists()) {
		usbi_err(NULL, "usb_raw not published");
		return;
	}

	Run();
	fMessenger = new(std::nothrow) BMessenger(this);
	if (fMessenger == NULL) {
		usbi_err(NULL, "error creating BMessenger object");
		return;
	}

	if (Lock()) {
		entry_ref ref;
		entry.GetRef(&ref);
		fRoot = new(std::nothrow) WatchedEntry(fMessenger, &ref);
		Unlock();
		if (fRoot == NULL)
			return;
		if (fRoot->InitCheck() == false) {
			delete fRoot;
			fRoot = NULL;
			return;
		}
	}
	fInitCheck = true;
}


void
RosterLooper::Stop()
{
	Lock();
	delete fRoot;
	delete fMessenger;
	Quit();
}


void
RosterLooper::MessageReceived(BMessage *message)
{
	int32 opcode;
	if (message->FindInt32("opcode", &opcode) < B_OK)
		return;

	switch (opcode) {
		case B_ENTRY_CREATED:
		{
			dev_t device;
			ino_t directory;
			const char *name;
			if (message->FindInt32("device", &device) < B_OK ||
				message->FindInt64("directory", &directory) < B_OK ||
				message->FindString("name", &name) < B_OK)
				break;

			entry_ref ref(device, directory, name);
			fRoot->EntryCreated(&ref);
			break;
		}
		case B_ENTRY_REMOVED:
		{
			ino_t node;
			if (message->FindInt64("node", &node) < B_OK)
				break;
			fRoot->EntryRemoved(node);
			break;
		}
	}
}


bool
RosterLooper::InitCheck()
{
	return fInitCheck;
}


USBRoster::USBRoster()
	:	fLooper(NULL)
{
}


USBRoster::~USBRoster()
{
	Stop();
}


int
USBRoster::Start()
{
	if (fLooper == NULL) {
		fLooper = new(std::nothrow) RosterLooper(this);
		if (fLooper == NULL || ((RosterLooper *)fLooper)->InitCheck() == false) {
			if (fLooper)
				fLooper = NULL;
			return LIBUSB_ERROR_OTHER;
		}
	}
	return LIBUSB_SUCCESS;
}


void
USBRoster::Stop()
{
	if (fLooper) {
		((RosterLooper *)fLooper)->Stop();
		fLooper = NULL;
	}
}
