/*
    PIP - Platform Independent Primitives
    USB, based on libusb
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "piusb.h"

#include "piconfig.h"
#include "piincludes_p.h"

#ifdef PIP_USB
#  ifdef WINDOWS
#	include <lusb0_usb.h>
#  else
#	include <usb.h>
#  endif
#endif

REGISTER_DEVICE(PIUSB)


PIUSB::PIUSB(ushort vid, ushort pid): PIIODevice("", ReadWrite) {
	vid_       = vid;
	pid_       = pid;
	intefrace_ = 0;
	hdev       = 0;
	timeout_r = timeout_w = 0;
	interface_claimed     = -1;
	setPath(PIString::fromNumber(vid_, 16).expandLeftTo(4, '0') + ":" + PIString::fromNumber(pid_, 16).expandLeftTo(4, '0'));
	setDeviceNumber(1);
	setTimeoutRead(1000);
	setTimeoutWrite(1000);
}


PIUSB::~PIUSB() {
	stop();
	close();
}

void PIUSB::Endpoint::parse() {
	direction     = (Direction)((address >> 7) & 1);
	transfer_type = (TransferType)(attributes & 3);
	if (transfer_type == Isochronous) {
		synchronisation_type = (SynchronisationType)((attributes >> 2) & 3);
		usage_type           = (UsageType)((attributes >> 4) & 3);
	}
}


PIUSB::Endpoint PIUSB::getEndpointByAddress(uchar address) {
	piForeachC(Endpoint & i, eps)
		if (i.address == address) return i;
	return Endpoint();
}


void PIUSB::setVendorID(ushort vid) {
	vid_ = vid;
	setPath(PIString::fromNumber(vid_, 16).expandLeftTo(4, '0') + ':' + PIString::fromNumber(pid_, 16).expandLeftTo(4, '0'));
}


void PIUSB::setProductID(ushort pid) {
	pid_ = pid;
	setPath(PIString::fromNumber(vid_, 16).expandLeftTo(4, '0') + ':' + PIString::fromNumber(pid_, 16).expandLeftTo(4, '0'));
}


PIVector<PIUSB::Endpoint> PIUSB::endpointsRead() {
	PIVector<Endpoint> ret;
	piForeachC(Endpoint & i, eps)
		if (i.direction == Endpoint::Read) ret << i;
	return ret;
}


PIVector<PIUSB::Endpoint> PIUSB::endpointsWrite() {
	PIVector<Endpoint> ret;
	piForeachC(Endpoint & i, eps)
		if (i.direction == Endpoint::Write) ret << i;
	return ret;
}


bool PIUSB::setConfiguration(uchar value) {
#ifdef PIP_USB
	if (hdev == 0) return false;
	bool found = false;
	piForeachC(Configuration & c, desc_.configurations)
		if (c.value_to_select == value) {
			found = true;
			conf_ = c;
			break;
		}
	if (!found) {
		piCoutObj << "Can`t find configuration with \"value_to_select\" =" << value;
		return false;
	}
	if (interface_claimed >= 0) usb_release_interface(hdev, interface_claimed);
	interface_claimed = -1;
	return setInterface(conf_.interfaces.front().value_to_select);
#else
	return false;
#endif
}


bool PIUSB::setInterface(uchar value) {
#ifdef PIP_USB
	if (hdev == 0) return false;
	bool found = false;
	piForeachC(Interface & i, conf_.interfaces)
		if (i.value_to_select == value) {
			found  = true;
			iface_ = i;
			break;
		}
	if (!found) {
		piCoutObj << "Can`t find interface with \"value_to_select\" =" << value;
		return false;
	}
	if (interface_claimed >= 0) usb_release_interface(hdev, interface_claimed);
	interface_claimed = -1;
	if (usb_claim_interface(hdev, iface_.value_to_select) < 0) {
		piCoutObj << "Error: Cant`t claim interface!";
		return false;
	}
	eps.clear();
	eps     = iface_.endpoints;
	ep_read = ep_write = Endpoint();
	for (int i = 0; i < eps.size_s(); ++i) {
		if (eps[i].direction == Endpoint::Read && ep_read.isNull()) ep_read = eps[i];
		if (eps[i].direction == Endpoint::Write && ep_write.isNull()) ep_write = eps[i];
	}
	interface_claimed = value;
	return true;
#else
	return false;
#endif
}


bool PIUSB::configureDevice(const void * e_main, const void * e_parent) {
#ifdef PIP_USB
	PIConfig::Entry * em = (PIConfig::Entry *)e_main;
	PIConfig::Entry * ep = (PIConfig::Entry *)e_parent;
	PIString vp          = readDeviceSetting<PIString>("device", "", em, ep);
	ushort v, p;
	if (vp.isEmpty()) {
		v = readDeviceSetting<ushort>("vid", vendorID(), em, ep);
		p = readDeviceSetting<ushort>("pid", productID(), em, ep);
	} else {
		v = vp.left(vp.find(":")).toInt(16);
		p = vp.right(vp.length() - vp.find(":") - 1).toInt(16);
	}
	setVendorID(v);
	setProductID(p);
	setDeviceNumber(readDeviceSetting<int>("deviceNumber", deviceNumber(), em, ep));
	setConfiguration(readDeviceSetting<ushort>("configuration", currentConfiguration().value_to_select, em, ep));
	setInterface(readDeviceSetting<ushort>("interface", currentInterface().value_to_select, em, ep));
	setEndpointRead(Endpoint(readDeviceSetting<ushort>("endpointRead", endpointRead().address, em, ep)));
	setEndpointWrite(Endpoint(readDeviceSetting<ushort>("endpointWrite", endpointWrite().address, em, ep)));
	return true;
#else
	return false;
#endif
}


bool PIUSB::openDevice() {
#ifdef PIP_USB
	if (path().size_s() >= 8) {
		vid_ = path().left(4).toInt(16);
		pid_ = path().right(4).toInt(16);
	}
	if (hdev != 0) closeDevice();
	hdev              = 0;
	interface_claimed = -1;
	ep_write = ep_read = Endpoint();
	usb_init();
	// usb_set_debug(4);
	if (usb_find_busses() < 0) {
		piCoutObj << "Error: Cant`t find busses!";
		return false;
	}
	if (usb_find_devices() < 0) {
		piCoutObj << "Error: Cant`t find devices!";
		return false;
	}

	// piCoutObj << "Search for device ... " << flush;
	int cur_num = 1;
	bool found  = false;
	struct usb_device * dev;
	struct usb_bus * bus;
	for (bus = usb_get_busses(); bus; bus = bus->next) {
		for (dev = bus->devices; dev; dev = dev->next) {
			if (dev->descriptor.idVendor == vid_ && dev->descriptor.idProduct == pid_) {
				if (cur_num == deviceNumber()) {
					struct usb_device_descriptor & dd(dev->descriptor);
					desc_.usb_spec_number    = dd.bcdUSB;
					desc_.device_class       = dd.bDeviceClass;
					desc_.device_subclass    = dd.bDeviceSubClass;
					desc_.device_protocol    = dd.bDeviceProtocol;
					desc_.max_packet_size    = dd.bMaxPacketSize0;
					desc_.id_vendor          = dd.idVendor;
					desc_.id_product         = dd.idProduct;
					desc_.id_device_release  = dd.bcdDevice;
					desc_.index_manufacturer = dd.iManufacturer;
					desc_.index_product      = dd.iProduct;
					desc_.index_serial       = dd.iSerialNumber;
					desc_.configurations.clear();
					for (int c = 0; c < dd.bNumConfigurations; ++c) {
						desc_.configurations << Configuration();
						Configuration & conf(desc_.configurations.back());
						struct usb_config_descriptor & dc(dev->config[c]);
						conf.index           = c;
						conf.value_to_select = dc.bConfigurationValue;
						conf.attributes      = dc.bmAttributes;
						conf.max_power       = ushort(dc.MaxPower) * 2;
						conf.self_powered    = (conf.attributes >> 6) & 1;
						conf.remote_wakeup   = (conf.attributes >> 5) & 1;
						conf.interfaces.clear();
						for (int i = 0; i < dc.bNumInterfaces; ++i) {
							conf.interfaces << Interface();
							Interface & infc(conf.interfaces.back());
							struct usb_interface_descriptor * di(dc.interface[i].altsetting);
							infc.index           = i;
							infc.value_to_select = di->bAlternateSetting;
							infc.class_code      = di->bInterfaceClass;
							infc.subclass_code   = di->bInterfaceSubClass;
							infc.protocol_code   = di->bInterfaceProtocol;
							infc.endpoints.clear();
							for (int e = 0; e < di->bNumEndpoints; ++e) {
								infc.endpoints << Endpoint(di->endpoint[e].bEndpointAddress,
								                           di->endpoint[e].bmAttributes,
								                           di->endpoint[e].wMaxPacketSize);
							}
						}
					}
					if (!desc_.configurations.isEmpty()) conf_ = desc_.configurations.front();

					struct usb_interface_descriptor * is = dev->config->interface->altsetting;
					int epn                              = is->bNumEndpoints;
					eps.clear();
					for (int i = 0; i < epn; ++i) {
						eps << Endpoint(is->endpoint[i].bEndpointAddress, is->endpoint[i].bmAttributes, is->endpoint[i].wMaxPacketSize);
						if (eps.back().direction == Endpoint::Write && (eps.back().address == ep_write.address || ep_write.address == 0))
							ep_write = eps.back();
						if (eps.back().direction == Endpoint::Read && (eps.back().address == ep_read.address || ep_read.address == 0))
							ep_read = eps.back();
					}

					// piCoutObj << "Device found at address:" << "Bus: " << dev->bus->dirname << ", Device: " << dev->filename;
					found = true;
					break;
				} else
					cur_num++;
			}
		}
		if (found) break;
	}
	if (!found) {
		piCoutObj << "Error: Cant`t find device!";
		return false;
	}
	// piCoutObj << "Open ... " << flush;
	hdev = usb_open(dev);
	if (hdev == 0) {
		piCoutObj << "Error: Cant`t open device:" << usb_strerror();
		return false;
	} // else piCoutObj << "ok";
	  // usb_reset(hdev);

	// usb_set_configuration(hdev, 1);
	// usb_set_altinterface(hdev, 0);

#  ifndef WINDOWS
	char tbuff[256];
	// piCoutObj << "Check for bounded driver ... " << flush;
	if (usb_get_driver_np(hdev, intefrace_, tbuff, sizeof(tbuff) - 1) >= 0) {
		// piCoutObj << "yes" << "Found driver: " << tbuff;
		// piCoutObj << "Detach driver ... " << flush;
		if (usb_detach_kernel_driver_np(hdev, intefrace_) < 0) {
			piCoutObj << "Error: Cant`t detach bounded driver!";
			return false;
		} // else piCoutObj << "ok";
	}     // else piCoutObj << "no";
#  endif

	// piCoutObj << "Claim interface ... " << flush;
	if (usb_claim_interface(hdev, intefrace_) < 0) {
		piCoutObj << "Error: Cant`t claim interface:" << usb_strerror();
		return false;
	} // else piCoutObj << "ok";
	interface_claimed = intefrace_;

	return true;
#else
	return false;
#endif
}


bool PIUSB::closeDevice() {
#ifdef PIP_USB
	if (hdev == 0) return true;
	if (interface_claimed >= 0) usb_release_interface(hdev, interface_claimed);
	usb_close(hdev);
	hdev              = 0;
	interface_claimed = -1;
	return true;
#else
	return false;
#endif
}


ssize_t PIUSB::readDevice(void * read_to, ssize_t max_size) {
#ifdef PIP_USB
	if (isClosed() || ep_read.isNull()) return -1;
	ssize_t ret = -1;
	// reading_now = true;
	switch (ep_read.transfer_type) {
	case Endpoint::Bulk:
		/*piCoutObj << "bulk read" << max_size;*/
		ret = usb_bulk_read(hdev, ep_read.address, (char *)read_to, max_size, timeout_r);
		break;
	case Endpoint::Interrupt: ret = usb_interrupt_read(hdev, ep_read.address, (char *)read_to, max_size, timeout_r);
	default: break;
	}
	// reading_now = false;
	return ret;
#else
	return -1;
#endif
}


ssize_t PIUSB::writeDevice(const void * data, ssize_t max_size) {
#ifdef PIP_USB
	if (isClosed() || ep_write.isNull()) return -1;
	switch (ep_write.transfer_type) {
	case Endpoint::Bulk:
		/*piCoutObj << "bulk write" << max_size;*/
		return usb_bulk_write(hdev, ep_write.address, (char *)const_cast<void *>(data), max_size, timeout_w);
	case Endpoint::Interrupt: return usb_interrupt_write(hdev, ep_write.address, (char *)data, max_size, timeout_w);
	default: break;
	}
	return -1;
#else
	return -1;
#endif
}


int PIUSB::controlWrite(const void * data, int max_size) {
#ifdef PIP_USB
	if (isClosed()) return -1;
	return -1;
#else
	return -1;
#endif
}


void PIUSB::flush() {
#ifdef PIP_USB
	if (isClosed()) return;
	if (!ep_read.isNull()) usb_resetep(hdev, ep_read.address);
	if (!ep_write.isNull()) usb_resetep(hdev, ep_write.address);
#endif
}


PICout operator<<(PICout s, const PIUSB::Endpoint & v) {
	s.saveAndSetControls(0);
	s << PICoutManipulators::NewLine << "{" << PICoutManipulators::NewLine;
	if (v.isNull())
		s << "  "
		  << "Null Endpoint";
	else {
		s << "  "
		  << "Address: " << v.address << PICoutManipulators::NewLine;
		s << "  "
		  << "Attributes: " << v.attributes << PICoutManipulators::NewLine;
		s << "    "
		  << "Direction: " << (v.direction == PIUSB::Endpoint::Write ? "Write" : "Read") << PICoutManipulators::NewLine;
		s << "    "
		  << "Transfer Type: ";
		switch (v.transfer_type) {
		case PIUSB::Endpoint::Control: s << "Control" << PICoutManipulators::NewLine; break;
		case PIUSB::Endpoint::Bulk: s << "Bulk" << PICoutManipulators::NewLine; break;
		case PIUSB::Endpoint::Interrupt: s << "Interrupt" << PICoutManipulators::NewLine; break;
		case PIUSB::Endpoint::Isochronous: s << "Isochronous" << PICoutManipulators::NewLine; break;
		default: break;
		}
		if (v.transfer_type == PIUSB::Endpoint::Isochronous) {
			s << "    "
			  << "Synchronisation Type: ";
			switch (v.synchronisation_type) {
			case PIUSB::Endpoint::NoSynchonisation: s << "No Synchonisation" << PICoutManipulators::NewLine; break;
			case PIUSB::Endpoint::Asynchronous: s << "Asynchronous" << PICoutManipulators::NewLine; break;
			case PIUSB::Endpoint::Adaptive: s << "Adaptive" << PICoutManipulators::NewLine; break;
			case PIUSB::Endpoint::Synchronous: s << "Synchronous" << PICoutManipulators::NewLine; break;
			default: break;
			}
			s << "    "
			  << "Usage Type: ";
			switch (v.usage_type) {
			case PIUSB::Endpoint::DataEndpoint: s << "Data Endpoint" << PICoutManipulators::NewLine; break;
			case PIUSB::Endpoint::FeedbackEndpoint: s << "Feedback Endpoint" << PICoutManipulators::NewLine; break;
			case PIUSB::Endpoint::ExplicitFeedbackDataEndpoint:
				s << "Explicit Feedback Data Endpoint" << PICoutManipulators::NewLine;
				break;
			default: break;
			}
		}
		s << "  "
		  << "Max Packet Size: " << v.max_packet_size << PICoutManipulators::NewLine;
	}
	s << "}" << PICoutManipulators::NewLine;
	s.restoreControls();
	return s;
}


PIString PIUSB::constructFullPathDevice() const {
	PIString ret;
	ret = PIString::fromNumber(vendorID(), 16).toLowerCase() + ":" + PIString::fromNumber(productID(), 16).toLowerCase() + ":" +
	      deviceNumber() + ":" + endpointRead().address + ":" + endpointWrite().address;
	return ret;
}


void PIUSB::configureFromFullPathDevice(const PIString & full_path) {
	PIStringList pl = full_path.split(":");
	for (int i = 0; i < pl.size_s(); ++i) {
		PIString p(pl[i]);
		switch (i) {
		case 0: setVendorID(p.toUShort(16)); break;
		case 1: setProductID(p.toUShort(16)); break;
		case 2: setDeviceNumber(p.toInt()); break;
		case 3: setEndpointRead(Endpoint(p.toInt())); break;
		case 4: setEndpointWrite(Endpoint(p.toInt())); break;
		}
	}
}
