#include "Controls.h"

#include "Settings.h"
#include "Board.h"

#include "Images.h"

////////////////////////////////////////////////
// BoardCtrl
////////////////////////////////////////////////

// callbacks
void BoardCtrl::dropCb(void)
{
	// get list of all available boards
	ArrayMap<String, Board*> const &boards = Settings.GetAvailableBoards();

	// refill the list
	dropList.ClearList();
	for(int iBoard = 0; iBoard < boards.GetCount(); iBoard++)
	{
		String name = boards.GetKey(iBoard);

		String desc = boards[iBoard]->GetDescription();
		dropList.Add(name, desc);

		int idx = dropList.FindKey(Settings.GetActiveBoard());
		if(idx >= 0)
			dropList.SetIndex(idx);
		else
			dropList.Adjust();
	}
}

// selection
void BoardCtrl::selCb(void)
{
	String boardName;
	int idx = dropList.GetIndex();
	if(idx < 0)
	{
		if(dropList.GetCount())
			boardName = dropList.GetKey(0);
		else
			boardName = "";
	}
	else
		boardName = dropList.GetKey(idx);
	WhenBoard(boardName);
}

// options submenu
void BoardCtrl::optionsSubMenu(Bar &bar, int curItem)
{
	Board *board = Settings.GetSelectedBoard();
	VectorMap<String, Board::BoardMenuItem> &menuItems = board->GetMenuItems();
	Board::BoardMenuItem &item = menuItems[curItem];
	VectorMap<String, String> &descs = item._descriptions;
	for(int i = 0; i < descs.GetCount(); i++)
	{
		Bar::Item &barItem = bar.Add(descs[i], [=, &menuItems] { menuItems[curItem]._selectedItem = i; });
		barItem.Check(item._selectedItem == i);
	}
}

// option menu
void BoardCtrl::optionsMenu(Bar &bar)
{
	Board *board = Settings.GetSelectedBoard();
	if(!board)
		return;
	VectorMap<String, Board::BoardMenuItem> &menuItems = board->GetMenuItems();
	for(int i = 0; i < menuItems.GetCount(); i++)
		bar.Add(menuItems.GetKey(i), THISBACK1(optionsSubMenu, i));
}

// options button menu
void BoardCtrl::optionsCb(void)
{
	Point p = boardOptionsBtn.GetScreenRect().BottomLeft();
	MenuBar::Execute(THISBACK(optionsMenu), p);
}
	

// constructor
BoardCtrl::BoardCtrl()
{
	CtrlLayout(*this);
	
	dropList.WhenDrop = THISBACK(dropCb);
	dropList.WhenAction = THISBACK(selCb);
	boardOptionsBtn.SetImage(FishIDEImg::Layers());
	boardOptionsBtn <<= THISBACK(optionsCb);
/*
	dropCb();
	dropList.Set(Settings.GetActiveBoard());
*/
}

// destructor
BoardCtrl::~BoardCtrl()
{
}

BoardCtrl &BoardCtrl::Set(String const &board)
{
	int idx = dropList.FindKey(board);
	if(idx >= 0)
		dropList.Set(dropList.GetValue(idx));
	else
		dropList.Set("");
	return *this;
}

String BoardCtrl::GetBoardName(void) const
{
	return dropList.Get();
}

//////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////
// DeviceCtrl
////////////////////////////////////////////////

// small helper
static String StripTrailingIP(String const &s)
{
	int i = s.GetCount() - 1;
	int dashCount = 0;
	char c = 0;
	while(i && dashCount < 4)
	{
		c = s[i];
		
		if(isdigit(c))
			i--;
		else if(c == '-')
		{
			i--;
			dashCount++;
		}
		else
			break;
	}
	if(dashCount == 4 && c == '-')
		return s.Left(i + 1);
	return s; 
}

// callbacks
void DeviceCtrl::dropCb(void)
{
	String s = ~dropChoice;
	ArrayMap<String, String> devices = Serial::GetSerialPorts();
	dropChoice.ClearList();
	for(int iDev = 0; iDev < devices.GetCount(); iDev++)
		dropChoice.Add(devices.GetKey(iDev));
	
	// if network is enabled, add also network ports
	if(_netEnabled)
	{
		Array<MDNSService> services = MDNS.GetDiscoveredServices("arduino", "tcp");
		for(int i = 0; i < services.GetCount(); i++)
		{
			MDNSService const &service = services[i];
			String host = StripTrailingIP(service.GetHostName());
			IPV4Address ip = service.GetIP();
			uint16_t port = service.GetPort();
			bool avail = (service.GetText("tcp_check") != "yes");
			if(!avail)
			{
				TcpSocket sock;
				sock.Timeout(100);
				if(sock.Connect(ip.ToString(), port) && sock.WaitConnect())
					avail = true;
				sock.Close();
			}
			if(avail)
				dropChoice.Add(host + "@" + ip.ToString() + ":" + FormatInt(port));
		}
	}

/*
	// this to make list drop even if empty on next calls... sigh
	if(!dropChoice.GetCount())
		dropChoice.Add(t_("<No ports found>"));
*/
}

void DeviceCtrl::pushCb(void)
{
	dropChoice.Clear();
	dropCb();
	if(dropChoice.GetCount())
		dropChoice.Drop();
}

// selection
void DeviceCtrl::selCb(void)
{
	_lastDevice = ~dropChoice;
	WhenDevice(_lastDevice);
}

// check if network device is responding
bool DeviceCtrl::checkNetDevice(IPV4Address &ip, int port)
{
	TcpSocket sock;
	sock.Timeout(100);
	bool avail = false;
	if(sock.Connect(ip.ToString(), port) && sock.WaitConnect())
		avail = true;
	sock.Close();
	return avail;
}

void DeviceCtrl::refreshDevice(void)
{
	// device format : host@ip:port
	String curDevice = ~dropChoice;
	if(curDevice.IsEmpty())
		curDevice = _lastDevice;

	// we're just checking network ports
	int atIdx = curDevice.Find("@");
	int colonIdx = curDevice.Find(":");
	if(atIdx < 0 || colonIdx < 0 || atIdx > colonIdx)
		return;
	
	if(_netEnabled)
	{
		String curHost = curDevice.Left(atIdx);
		String rem = curDevice.Mid(atIdx + 1);
		colonIdx -= (atIdx + 1);
		IPV4Address curIP = IPV4Address(rem.Left(colonIdx));
		rem = rem.Mid(colonIdx + 1);
		int curPort = ScanInt(rem);

		Array<MDNSService> services = MDNS.GetDiscoveredServices("arduino", "tcp");

		// first check if current device is still there
		for(int i = 0; i < services.GetCount(); i++)
		{
			MDNSService const &service = services[i];
			String host = StripTrailingIP(service.GetHostName());
			IPV4Address ip = service.GetIP();
			uint16_t port = service.GetPort();
			
			if(host != curHost || ip != curIP || port != curPort)
				continue;

			// if current device is still available, just return
			bool avail = (service.GetText("tcp_check") != "yes");
			if(!avail)
				avail = checkNetDevice(ip, port);
			if(avail)
				return;
		}
		
		// current device not available, look if we had just an IP change
		// erase current device
		dropChoice = "";
		
		// now check if a similar device appeared
		for(int i = 0; i < services.GetCount(); i++)
		{
			MDNSService const &service = services[i];
			String host = StripTrailingIP(service.GetHostName());
			if(host != curHost)
				continue;
			
			// found a device with same hostname, check it
			IPV4Address ip = service.GetIP();
			uint16_t port = service.GetPort();
			bool avail = (service.GetText("tcp_check") != "yes");
			if(!avail)
				avail = checkNetDevice(ip, port);
			if(!avail)
				continue;
			
			// the device is there and responding
			// use it
			_lastDevice = host + "@" + ip.ToString() + ":" + FormatInt(port);
			
			// ensure that device is inside list; if not, add it at end
			int idx = dropChoice.FindKey(_lastDevice);
			if(idx < 0)
				dropChoice.Add(_lastDevice);
			dropChoice = _lastDevice;
			WhenDevice(_lastDevice);
		}
	}
}


// constructor
DeviceCtrl::DeviceCtrl()
{
	CtrlLayout(*this);
	dropChoice.NoWantFocus();
	dropChoice.AlwaysDrop();
	dropChoice.WhenDrop = THISBACK(dropCb);
	dropChoice.WhenAction = THISBACK(selCb);
	dropCb();
	dropChoice <<= Settings.GetDevice();
	
	_netEnabled = false;
}

// destructor
DeviceCtrl::~DeviceCtrl()
{
}

// enable/disable network ports
DeviceCtrl &DeviceCtrl::EnableNetwork(bool en)
{
	_netEnabled = en;
	return *this;
}

DeviceCtrl &DeviceCtrl::Set(String const &device)
{
	dropChoice <<= device;
	_lastDevice = device;
	refreshDevice();
	return *this;
}

String DeviceCtrl::GetDevice(void) const
{
	return _lastDevice;
}

////////////////////////////////////////////////
// SerialMonitorCtrl
////////////////////////////////////////////////

// constructor
SerialMonitorCtrl::SerialMonitorCtrl()
{
	CtrlLayout(*this);
	deviceContainer.AddChild(&deviceCtrl.SizePos());
	sendContainer.AddChild(&sendEdit.SizePos());
	sendEdit.WhenKey = THISBACK(sendCb);

	// fill baudrates list
	Index<dword> const &bauds = Serial::GetStandardBaudRates();
	speedDrop.Clear();
	for(int i = 0; i < bauds.GetCount(); i++)
		speedDrop.Add((int)bauds[i]);
	speedDrop = 9600;

	openBtn.Enable();
	closeBtn.Disable();
	deviceCtrl.Enable();
	speedDrop.Enable();
	sendEdit.Disable();
	receiveEdit.SetReadOnly();
	
	openBtn << THISBACK(openCb);
	closeBtn << THISBACK(closeCb);
	clearBtn << THISBACK(clearCb);
	
	deviceCtrl.WhenDevice << THISBACK(deviceCb);
	speedDrop.WhenAction << THISBACK(changeCb);
}

// destructor
SerialMonitorCtrl::~SerialMonitorCtrl()
{
	KillTimeCallback(TIMEID_PERIODIC);
	serial.Close();
}

void SerialMonitorCtrl::openCb(void)
{
	if(serial.IsOpened())
		return;
	if(serial.Open(deviceCtrl.GetDevice(), (int)~speedDrop))
	{
		openBtn.Disable();
		closeBtn.Enable();
		deviceCtrl.Disable();
		speedDrop.Disable();
		sendEdit.Enable();
		SetTimeCallback(10, THISBACK(pollCb), TIMEID_PERIODIC);
	}
}

void SerialMonitorCtrl::closeCb(void)
{
	KillTimeCallback(TIMEID_PERIODIC);
	serial.Close();
	openBtn.Enable();
	closeBtn.Disable();
	deviceCtrl.Enable();
	speedDrop.Enable();
	sendEdit.Disable();
}

void SerialMonitorCtrl::pollCb(void)
{
	String s = serial.Read();
	if(!s.IsEmpty())
	{
		receiveEdit.Set(receiveEdit.Get() + s);
		receiveEdit.ScrollEnd();
	}
	SetTimeCallback(10, THISBACK(pollCb), TIMEID_PERIODIC);
}

void SerialMonitorCtrl::sendCb(dword key)
{
	if(serial.IsOpened() && key < 65536)
	{
		String s;
		s.Cat(key);
		if(key == 0x0d)
			s.Cat(0x0a);
		serial.Write(s);
	}
}

void SerialMonitorCtrl::clearCb(void)
{
	sendEdit.Clear();
	receiveEdit.Clear();
}

void SerialMonitorCtrl::changeCb(void)
{
	WhenChange();
}

void SerialMonitorCtrl::deviceCb(String const &dev)
{
	WhenChange();
}
	
// streaming
void SerialMonitorCtrl::Xmlize(XmlIO &xml)
{
	String content;
	String port;
	int speed;

	if(xml.IsStoring())
	{
		port = deviceCtrl.GetDevice();
		speed = speedDrop.Get();
		content = ~receiveEdit;
	}
	xml
		("Port"			, port)
		("Speed"		, speed)
		("Content"		, content)
	;
	if(xml.IsLoading())
	{
		deviceCtrl.Set(port);
		speedDrop.Set(speed);
		receiveEdit <<= content;
	}
}

