/*
 * Copyright (c) 2007      Gustavo Pichorim Boiko <gustavo.boiko@kdemail.net>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "randrscreen.h"
#include "randrcrtc.h"
#include "randroutput.h"
#include "randrmode.h"

#include <KConfig>
#include <KConfigGroup>
#include <QAction>

RandRScreen::RandRScreen(int screenIndex)
: m_resources(0L)
{
	m_index = screenIndex;
	m_rect = QRect(0, 0, XDisplayWidth(QX11Info::display(), m_index),
				 XDisplayHeight(QX11Info::display(), m_index));

	m_connectedCount = 0;
	m_activeCount = 0;

	loadSettings();
	load();

	// select for randr input events
	int mask = RRScreenChangeNotifyMask | 
		   RRCrtcChangeNotifyMask   | 
		   RROutputChangeNotifyMask | 
		   RROutputPropertyNotifyMask;
	
	XRRSelectInput(QX11Info::display(), rootWindow(), 0);
	XRRSelectInput(QX11Info::display(), rootWindow(), mask); 
}

RandRScreen::~RandRScreen()
{
	if (m_resources)
		XRRFreeScreenResources(m_resources);

	//qDeleteAll(m_crtcs);
	//qDeleteAll(m_outputs);
	//qDeleteAll(m_modes);
}

int RandRScreen::index() const
{
	return m_index;
}

XRRScreenResources* RandRScreen::resources() const
{
	return m_resources;
}

Window RandRScreen::rootWindow() const
{
	return RootWindow(QX11Info::display(), m_index);
}

void RandRScreen::loadSettings(bool notify)
{
	bool changed = false;
	int minW, minH, maxW, maxH;

	Status status = XRRGetScreenSizeRange(QX11Info::display(), rootWindow(),
					 &minW, &minH, &maxW, &maxH);
	//FIXME: we should check the status here
	Q_UNUSED(status);
	QSize minSize = QSize(minW, minH);
	QSize maxSize = QSize(maxW, maxH);

	if (minSize != m_minSize || maxSize != m_maxSize)
	{
		m_minSize = minSize;
		m_maxSize = maxSize;
		changed = true;
	}

	if (m_resources)
		XRRFreeScreenResources(m_resources);

	m_resources = XRRGetScreenResources(QX11Info::display(), rootWindow());
	Q_ASSERT(m_resources);

	RandR::timestamp = m_resources->timestamp;

	// get all modes
	for (int i = 0; i < m_resources->nmode; ++i)
	{
		if (!m_modes.contains(m_resources->modes[i].id))
		{
			m_modes[m_resources->modes[i].id] = RandRMode(&m_resources->modes[i]);
			changed = true;
		}
	}

	//get all crtcs
	kDebug() << "Creating CRTC object for XID 0 (\"None\")";
	RandRCrtc *c_none = new RandRCrtc(this, None);
	m_crtcs[None] = c_none;
	
	for (int i = 0; i < m_resources->ncrtc; ++i)
	{
		if (m_crtcs.contains(m_resources->crtcs[i]))
			m_crtcs[m_resources->crtcs[i]]->loadSettings(notify);
		else
		{
			kDebug() << "Creating CRTC object for XID" << m_resources->crtcs[i];
			RandRCrtc *c = new RandRCrtc(this, m_resources->crtcs[i]);
			connect(c, SIGNAL(crtcChanged(RRCrtc, int)), this, SIGNAL(configChanged()));
			connect(c, SIGNAL(crtcChanged(RRCrtc, int)), this, SLOT(save()));
			c->loadSettings(notify);
			m_crtcs[m_resources->crtcs[i]] = c;
			changed = true;
		}
	}

	//get all outputs
	for (int i = 0; i < m_resources->noutput; ++i)
	{
		if (m_outputs.contains(m_resources->outputs[i]))
			;//m_outputs[m_resources->outputs[i]]->loadSettings(notify);
		else
		{
			kDebug() << "Creating output object for XID" << m_resources->outputs[i];
			RandROutput *o = new RandROutput(this, m_resources->outputs[i]);
			connect(o, SIGNAL(outputChanged(RROutput, int)), this,
				      SLOT(slotOutputChanged(RROutput, int)));
			m_outputs[m_resources->outputs[i]] = o;
			if (o->isConnected())
				m_connectedCount++;
			if (o->isActive())
				m_activeCount++;

			changed = true;
		}
	}

	if (notify && changed)
		emit configChanged();

}

void RandRScreen::handleEvent(XRRScreenChangeNotifyEvent* event)
{
	m_rect.setWidth(event->width);
	m_rect.setHeight(event->height);

	emit configChanged();
}

void RandRScreen::handleRandREvent(XRRNotifyEvent* event)
{
	RandRCrtc *c;
	RandROutput *o;
	XRRCrtcChangeNotifyEvent *crtcEvent;
	XRROutputChangeNotifyEvent *outputEvent;
	XRROutputPropertyNotifyEvent *propertyEvent;

	// forward events to crtcs and outputs
	switch (event->subtype) {
		case RRNotify_CrtcChange:
			crtcEvent = (XRRCrtcChangeNotifyEvent*)event;
			c = crtc(crtcEvent->crtc);
			Q_ASSERT(c);
			c->handleEvent(crtcEvent);
			return;

		case RRNotify_OutputChange:
			outputEvent = (XRROutputChangeNotifyEvent*)event;
			o = output(outputEvent->output);
			Q_ASSERT(o);
			o->handleEvent(outputEvent);
			return;

		case RRNotify_OutputProperty:
			propertyEvent = (XRROutputPropertyNotifyEvent*)event;
			o = output(propertyEvent->output);
			Q_ASSERT(o);
			o->handlePropertyEvent(propertyEvent);
			return;
	}	
}

QSize RandRScreen::minSize() const
{
	return m_minSize;
}

QSize RandRScreen::maxSize() const
{
	return m_maxSize;
}

CrtcMap RandRScreen::crtcs() const
{
	return m_crtcs;
}

RandRCrtc* RandRScreen::crtc(RRCrtc id) const
{
	if (m_crtcs.contains(id))
		return m_crtcs[id];

	return 0;
}

OutputMap RandRScreen::outputs() const
{
	return m_outputs;
}

RandROutput* RandRScreen::output(RROutput id) const
{
	if (m_outputs.contains(id))
		return m_outputs[id];

	return 0;
}

ModeMap RandRScreen::modes() const
{
	return m_modes;
}

RandRMode RandRScreen::mode(RRMode id) const
{
	if (m_modes.contains(id))
		return m_modes[id];

	return RandRMode(0);
}

bool RandRScreen::adjustSize(const QRect &minimumSize)
{
	//try to find a size in which all outputs fit
	
	//start with a given minimum rect
	QRect rect = QRect(0, 0, 0, 0).united(minimumSize);

	foreach(RandROutput *output, m_outputs)
	{
		// outputs that are not active should not be taken into account
		// when calculating the screen size
		if (!output->isActive())
			continue;
		rect = rect.united(output->rect());
	}


	// check bounds
	if (rect.width() < m_minSize.width())
		rect.setWidth(m_minSize.width());
	if (rect.height() < m_minSize.height())
		rect.setHeight(m_minSize.height());

	if (rect.width() > m_maxSize.width())
		return false;
	if (rect.height() > m_maxSize.height())
		return false;

	return setSize(rect.size());
}

bool RandRScreen::setSize(const QSize &s)
{
	if (s == m_rect.size())
		return true;

	if (s.width() < m_minSize.width() || 
	    s.height() < m_minSize.height() ||
	    s.width() > m_maxSize.width() ||
	    s.height() > m_maxSize.height())
		return false;

	int widthMM, heightMM;
	float dpi;

	/* values taken from xrandr */
	dpi = (25.4 * DisplayHeight(QX11Info::display(), m_index)) / DisplayHeightMM(QX11Info::display(), m_index);
	widthMM =  (int) ((25.4 * s.width()) / dpi);
	heightMM = (int) ((25.4 * s.height()) / dpi);

	XRRSetScreenSize(QX11Info::display(), rootWindow(), s.width(), s.height(), widthMM, heightMM);
	m_rect.setSize(s);

	return true;
}

int RandRScreen::connectedCount() const
{
	return m_connectedCount;
}

int RandRScreen::activeCount() const
{
	return m_activeCount;
}

bool RandRScreen::outputsUnified() const
{
	return m_outputsUnified;
}

void RandRScreen::setOutputsUnified(bool unified)
{
	m_outputsUnified = unified;
	
	// should this be called here?
	slotUnifyOutputs(unified);
}

int RandRScreen::unifiedRotations() const
{

	bool first = true;
	int rotations = RandR::Rotate0;

	foreach(RandRCrtc *crtc, m_crtcs)
	{
		if (!crtc->connectedOutputs().count())
			continue;

		if (first)
		{
			rotations = crtc->rotations();
			first = false;
		}
		else
			rotations &= crtc->rotations();
	}

	return rotations;
}

SizeList RandRScreen::unifiedSizes() const
{
	SizeList sizeList;
	bool first = true;

	foreach(RandROutput *output, m_outputs)
	{
		if (!output->isConnected())
			continue;

		if (first)
		{
			// we start using the list from the first output
			sizeList = output->sizes();
			first = false;
		}
		else
		{
			SizeList outputSizes = output->sizes();
			for (int i = sizeList.count() - 1; i >=0; --i)
			{
				// check if the current output has the i-th size of the sizeList
				// if not, remove from the list
				if (outputSizes.indexOf(sizeList[i]) == -1)
					sizeList.removeAt(i);
			}
		}
	}

	return sizeList;
}

QRect RandRScreen::rect() const
{
	return m_rect;
}

void RandRScreen::load(KConfig &config)
{
	KConfigGroup group = config.group("Screen_" + QString::number(m_index));
	m_outputsUnified = group.readEntry("OutputsUnified", false);
	m_unifiedRect = (group.readEntry("UnifiedRect", "0,0,0,0") == "0,0,0,0")
		? QRect() // "0,0,0,0" (serialization for QRect()) does not convert to a QRect
		: group.readEntry("UnifiedRect", QRect());
	m_unifiedRotation = group.readEntry("UnifiedRotation", (int) RandR::Rotate0);

	slotUnifyOutputs(m_outputsUnified);

	foreach(RandROutput *output, m_outputs)
	{
		if (output->isConnected())
			output->load(config);
	}
}

void RandRScreen::save(KConfig &config)
{
	KConfigGroup group = config.group("Screen_" + QString::number(m_index));
	group.writeEntry("OutputsUnified", m_outputsUnified);
	group.writeEntry("UnifiedRect", m_unifiedRect);
	group.writeEntry("UnifiedRotation", m_unifiedRotation);

	foreach(RandROutput *output, m_outputs)
	{
		if (output->isConnected())
			output->save(config);
	}
}

void RandRScreen::save()
{
	KConfig cfg("krandrrc");
	save(cfg);
}

void RandRScreen::load()
{
	KConfig cfg("krandrrc");
	load(cfg);
}

bool RandRScreen::applyProposed(bool confirm)
{
	kDebug() << "Applying proposed changes for screen" << m_index << "...";
	
	bool succeed = true;
	QRect r;
	
	foreach(RandROutput *output, m_outputs) {
		/*
		r = output->rect();
		RandROutput::Relation outputRelation;
		RandROutput *related = output->relation(&outputRelation);
		
		if(!related) {
			r.setTopLeft(QPoint(0, 0));
		}
		QRect relativeRect = related->rect();
	
		switch(outputRelation) {
		case RandROutput::LeftOf:
			r.setTopLeft(QPoint(relativeRect.x() - r.x(),
			                    relativeRect.y()));
		case RandROutput::RightOf:
			r.setTopLeft(QPoint(relativeRect.x() + r.x(),
			                    relativeRect.y()));
		case RandROutput::Over:
			r.setTopLeft(QPoint(relativeRect.x(),
			                    relativeRect.y() - r.y()));
		case RandROutput::Under:
			r.setTopLeft(QPoint(relativeRect.x(),
			                    relativeRect.y() + r.y()));
							
		case RandROutput::SameAs:
			r.setTopLeft(related->rect().topLeft());
			break;
		}
		output->proposeRect(r);
		*/
		if(!output->applyProposed()) {
			succeed = false;
			break;
		}
	}
	/*
	foreach(RandROutput *output, m_outputs)
	{
		r = output->rect();
		if (!output->applyProposed())
		{
			succeed = false;
			break;
		}
	}*/

	kDebug() << "Changes have been applied to all outputs.";

	// if we could apply the config clean, ask for confirmation
	if (succeed && confirm)
		succeed = RandR::confirm(r);

	// if we succeeded applying and the user confirmed the changes,
	// just return from here 
	if (succeed)
		return true;

	kDebug() << "Changes canceled, reverting to original setup.";

	//Revert changes if not succeed
	foreach(RandROutput *o, m_outputs)
	{
		if (o->isConnected())
		{
			o->proposeOriginal();
			o->applyProposed();
		}
	}
	return false;
}

void RandRScreen::unifyOutputs()
{
	KConfig cfg("krandrrc");
	SizeList sizes = unifiedSizes();

	//FIXME: better handle this
	if (!sizes.count())
		return;

	// if there is only one output connected, there is no way to unify it
	if (m_connectedCount <= 1)
		return;

	if (sizes.indexOf(m_unifiedRect.size()) == -1)
		m_unifiedRect.setSize(sizes.first());

	kDebug() << "Unifying outputs using rect " << m_unifiedRect;
	// iterate over all outputs and make sure all connected outputs get activated
	// and use the right size
	foreach(RandROutput *o, m_outputs)
	{
		// if the output is not connected we don't need to do anything
		if (!o->isConnected())
		       continue;

		// if the output is connected and already has the same rect and rotation
		// as the unified ones, continue
		if (o->isActive() && o->rect() == m_unifiedRect 
				  && o->rotation() == m_unifiedRotation)
			continue;

		// this is to get the refresh rate to use
		//o->load(cfg);
		o->proposeRect(m_unifiedRect);
		o->proposeRotation(m_unifiedRotation);
		o->applyProposed(RandR::ChangeRect | RandR::ChangeRotation, false);
	}

	// FIXME: if by any reason we were not able to unify the outputs, we should 
	// do something
	save();
	emit configChanged();
}

void RandRScreen::slotResizeUnified(QAction *action)
{
	m_unifiedRect.setSize(action->data().toSize()); 
	unifyOutputs();
}

void RandRScreen::slotUnifyOutputs(bool unified)
{
	m_outputsUnified = unified;
	KConfig cfg("krandrrc");

	if (!unified || m_connectedCount <= 1)
	{
		foreach(RandROutput *output, m_outputs)
			if (output->isConnected())
			{
				output->load(cfg);
				output->applyProposed();
			}
	}
	else
	{
		SizeList sizes = unifiedSizes();

		if (!sizes.count())
		{
			// FIXME: this should be better handle
			return;
		}

		QSize s = m_unifiedRect.size();

		// if the last size we used is not available, use the first one
		// from the list
		if (sizes.indexOf(s) == -1)
			s = sizes[0];

		m_unifiedRect.setTopLeft(QPoint(0,0));
		m_unifiedRect.setSize(s);
		unifyOutputs();
	}
}

void RandRScreen::slotRotateUnified(QAction *action)
{
	m_unifiedRotation = action->data().toInt(); 
	
	unifyOutputs();
}

void RandRScreen::slotOutputChanged(RROutput id, int changes)
{
	Q_UNUSED(id);
	Q_UNUSED(changes);

	int connected = 0, active = 0;
	foreach(RandROutput *output, m_outputs)
	{
		if (output->isConnected())
			connected++;
		if (output->isActive())
			active++;
	}

	m_connectedCount = connected;
	m_activeCount = active;

	// if there is less than 2 outputs connected, there is no need to unify
	if (connected <= 1)
		return;

	// wait some time before checking the output configuration as some randr 
	// clients do operations in more than one step
	if(m_outputsUnified)
		unifyOutputs();
}

#include "randrscreen.moc"


