//
// CmdDialog.cc for pekwm
// Copyright (C) 2004-2005 Claes Nasten <pekdon{@}pekdon{.}net>
//
// This program is licensed under the GNU GPL.
// See the LICENSE file for more information.
//

#include "../config.h"

#include "PWinObj.hh"
#include "PDecor.hh"
#include "CmdDialog.hh"
#include "Config.hh"
#include "PScreen.hh"
#include "PixmapHandler.hh"
#include "KeyGrabber.hh"
#include "ScreenResources.hh"
#include "Workspaces.hh"

#include <list>

#ifdef DEBUG
#include <iostream>
using std::cerr;
using std::endl;
#endif // DEBUG

using std::list;

extern "C" {
#include <X11/Xutil.h> // XLookupString
#include <X11/keysym.h>
}

using std::string;

//! @brief CmdDialog constructor
//! @todo Initial size, configurable?
CmdDialog::CmdDialog(Display *dpy, Theme *theme, const std::string &title) : PDecor(dpy, theme, "CMDDIALOG"),
_cmd_data(theme->getCmdDialogData()),
_cmd_wo(NULL), _bg(None),
_wo_ref(NULL),
_pos(0), _buf_off(0), _buf_chars(0)
{
	// PWinObj attributes
	_type = PWinObj::WO_CMD_DIALOG;
	_layer = LAYER_NONE; // hack, goes over LAYER_MENU
	_hidden = true; // don't care about it when changing worskpace etc

	// add action to list, going to be used from exec
	::Action action;
	_ae.action_list.push_back(action);

	titleAdd(&_title);
	titleSetActive(0);
	setTitle(title);

	_cmd_wo = new PWinObj(_dpy);
	XSetWindowAttributes attr;
	attr.override_redirect = false;
	attr.event_mask = ButtonPressMask|ButtonReleaseMask|ButtonMotionMask|
		FocusChangeMask|KeyPressMask|KeyReleaseMask;
	_cmd_wo->setWindow(XCreateWindow(_dpy, _window,
										 0, 0, 1, 1, 0,
										 CopyFromParent, InputOutput, CopyFromParent,
										 CWOverrideRedirect|CWEventMask, &attr));

	addChild(_cmd_wo);
	activateChild(_cmd_wo);
	_cmd_wo->mapWindow();

	// setup texture, size etc
	loadTheme();

	Workspaces::instance()->insert(this);
	_wo_list.push_back(this);
}

//! @brief CmdDialog destructor
CmdDialog::~CmdDialog(void)
{
	Workspaces::instance()->remove(this);
	_wo_list.remove(this);

	// remove ourself from the decor manually, no need to reparent and stuff
	_child_list.remove(_cmd_wo);

	if (_cmd_wo != NULL) {
		XDestroyWindow(_dpy, _cmd_wo->getWindow());
		delete _cmd_wo;
	}

	unloadTheme();
}

//! @brief Handles ButtonPress, moving the text cursor
ActionEvent*
CmdDialog::handleButtonPress(XButtonEvent *ev)
{
	if (*_cmd_wo == ev->window) {
		// FIXME: move cursor
		return NULL;
	} else {
		return PDecor::handleButtonPress(ev);
	}
}

//! @brief Handles KeyPress, editing the buffer
ActionEvent*
CmdDialog::handleKeyPress(XKeyEvent *ev)
{
	ActionEvent *c_ae, *ae = NULL;

	if ((c_ae = KeyGrabber::instance()->findAction(ev, _type)) != NULL) {
		list<Action>::iterator it(c_ae->action_list.begin());
		for (; it != c_ae->action_list.end(); ++it) {
			switch (it->getAction()) {
			case CMD_D_INSERT:
				bufAdd(ev);
				break;
			case CMD_D_REMOVE:
				bufRemove();
				break;
			case CMD_D_CLEAR:
				bufClear();
				break;
			case CMD_D_EXEC:
				ae = exec();

				unmapWindow();
				bufClear();
				break;
			case CMD_D_CLOSE:
				unmapWindow();
				bufClear();
				break;
			case CMD_D_COMPLETE:
				break;
			case CMD_D_CURS_NEXT:
				bufChangePos(1);
				break;
			case CMD_D_CURS_PREV:
				bufChangePos(-1);
				break;
			case CMD_D_CURS_BEGIN:
				_pos = 0;
				break;
			case CMD_D_CURS_END:
				_pos = _buf.size();
				break;
			case CMD_D_HIST_NEXT:
				histNext();
				break;
			case CMD_D_HIST_PREV:
				histPrev();
				break;
			case CMD_D_NO_ACTION:
			default:
				// do nothing, shouldn't happen
				break;
			};
		}

		// something ( most likely ) changed, redraw the window
		if (ae == NULL) {
			bufChanged();
			render();
		}
	}

	return ae;
}

//! @brief Handles ExposeEvent, redraw when ev->count == 0
ActionEvent*
CmdDialog::handleExposeEvent(XExposeEvent *ev)
{
	if (ev->count > 0) {
		return NULL;
	}
	render();
	return NULL;
}

//! @brief Maps the CmdDialog center on the PWinObj it executes actions on.
void
CmdDialog::mapCenteredOnWORef(void)
{
	if (!_wo_ref) {
		_wo_ref = PWinObj::getRootPWinObj();
	}

	if (_wo_ref->getType() == PWinObj::WO_CLIENT) {
		_wo_ref = _wo_ref->getParent();
	}

	// Setup data
	_hist_it = _hist_list.end();

	// Make sure position is inside head.
	Geometry head;
	uint head_nr = PScreen::instance()->getNearestHead(_wo_ref->getX() + (_wo_ref->getWidth() / 2),
																										 _wo_ref->getY() + (_wo_ref->getHeight() / 2));
	PScreen::instance()->getHeadInfo(head_nr, head);

	// Make sure X is inside head.
	int new_x = _wo_ref->getX() + (static_cast<int>(_wo_ref->getWidth())
																 - static_cast<int>(_gm.width)) / 2;
	if (new_x < head.x)
		new_x = head.x;
	else if ((new_x + _gm.width) > (head.x + head.width))
		new_x = head.x + head.width - _gm.width;

	// Make sure Y is inside head.
	int new_y = _wo_ref->getY() + (static_cast<int>(_wo_ref->getHeight())
																	- static_cast<int>(_gm.height)) / 2;
	if (new_y < head.y)
		new_y = head.y;
	else if ((new_y + _gm.height) > (head.y + head.height))
		new_y = head.y + head.height - _gm.height;

	move(new_x, new_y);
			 
	PDecor::mapWindowRaised();
}

//! @brief Sets title of decor
void
CmdDialog::setTitle(const std::string &title)
{
	_title.setReal(title);
	_title.setVisible(title);
}

//! @brief Sets background and size
void
CmdDialog::loadTheme(void)
{
	// setup variables
	_cmd_data = _theme->getCmdDialogData();

	// setup size
	resizeChild(PScreen::instance()->getWidth() / 4,
							_cmd_data->getFont()->getHeight() +
							_cmd_data->getPad(DIRECTION_UP) +
							_cmd_data->getPad(DIRECTION_DOWN));

	// get pixmap
	PixmapHandler *pm = ScreenResources::instance()->getPixmapHandler();
	pm->returnPixmap(_bg);
	_bg = pm->getPixmap(_cmd_wo->getWidth(), _cmd_wo->getHeight(),
											PScreen::instance()->getDepth(), false);

	// render texture
	_cmd_data->getTexture()->render(_bg, 0, 0,
																	_cmd_wo->getWidth(), _cmd_wo->getHeight());
	_cmd_wo->setBackgroundPixmap(_bg);
	_cmd_wo->clear();
}

//! @brief Frees resources
void
CmdDialog::unloadTheme(void)
{
	ScreenResources::instance()->getPixmapHandler()->returnPixmap(_bg);
}

//! @brief Renders _buf onto _cmd_wo
void
CmdDialog::render(void)
{
	_cmd_wo->clear();

	// draw buf content
	_cmd_data->getFont()->setColor(_cmd_data->getColor());
	_cmd_data->getFont()->draw(_cmd_wo->getWindow(),
														 _cmd_data->getPad(DIRECTION_LEFT),
														 _cmd_data->getPad(DIRECTION_UP),
														 _buf.c_str() + _buf_off, _buf_chars);

	// draw cursor
	uint pos = _cmd_data->getPad(DIRECTION_LEFT);
	if (_pos > 0) {
		pos = _cmd_data->getFont()->getWidth(_buf.c_str() + _buf_off,
																				 _pos - _buf_off) + 1;
	}

	_cmd_data->getFont()->draw(_cmd_wo->getWindow(),
														 pos, _cmd_data->getPad(DIRECTION_UP),
														 "|");
}

//! @brief Parses _buf and tries to generate an ActionEvent
ActionEvent*
CmdDialog::exec(void)
{
	_hist_list.push_back(_buf);
	if (_hist_list.size() > 10) // FIXME: make configurable
		_hist_list.pop_front();

	// Check if it's a valid Action, if not we assume it's a command and try
	// to execute it.
	if (!Config::instance()->parseAction(_buf,	_ae.action_list.front(),
																			 KEYGRABBER_OK)) {
		_ae.action_list.front().setAction(ACTION_EXEC);
		_ae.action_list.front().setParamS(_buf);
	}

	return &_ae;
}

//! @brief
void
CmdDialog::complete(void)
{
}

//! @brief Adds char to buffer
void
CmdDialog::bufAdd(XKeyEvent *ev)
{
	KeySym ks;
	char c_return;

	XLookupString(ev, &c_return, 1, &ks, NULL);

	if (isprint(c_return) != 0) {
		_buf.insert(_buf.begin() + _pos++, c_return);
	}
}

//! @brief Removes char from buffer
void
CmdDialog::bufRemove(void)
{
	if ((_pos > _buf.size()) || (_pos == 0) || (_buf.size() == 0)) {
#ifdef DEBUG
		cerr << __FILE__ << "@" << __LINE__ << ": "
				 << "CmdDialog(" << this << ")::bufRemove()" << endl
				 << " *** _pos: " << _pos << " ,  _buf.size(): " << _buf.size() << endl;
#endif // DEBUG
		return;
	}

	_buf.erase(_buf.begin() + --_pos);
}

//! @brief Clears the buffer, resets status
void
CmdDialog::bufClear(void)
{
	_buf = ""; // old gcc doesn't know about .clear()
	_pos = _buf_off = _buf_chars = 0;
}

//! @brief Moves the marker
void
CmdDialog::bufChangePos(int off)
{
	if ((signed(_pos) + off) < 0) {
		_pos = 0;
	} else if (unsigned(_pos + off) > _buf.size()) {
		_pos = _buf.size();
	} else {
		_pos += off;
	}
}

//! @brief Recalculates, _buf_off and _buf_chars
void
CmdDialog::bufChanged(void)
{
	PFont *font =  _cmd_data->getFont(); // convenience

	// complete string doesn't fit in the window OR
	// we don't fit in the first set
	if ((_pos > 0)
			&& (font->getWidth(_buf.c_str()) > _cmd_wo->getWidth())
			&& (font->getWidth(_buf.c_str(), _pos) > _cmd_wo->getWidth())) {

		// increase position until it all fits
		for (_buf_off = 0; _buf_off < _pos; ++_buf_off) {
			if (font->getWidth(_buf.c_str() + _buf_off, _buf.size() - _buf_off)
					< _cmd_wo->getWidth())
				break;
		}

		_buf_chars = _buf.size() - _buf_off;
	} else {
		_buf_off = 0;
		_buf_chars = _buf.size();
	}
}

//! @brief
void
CmdDialog::histNext(void)
{
	if (_hist_it == _hist_list.end()) {
		return; // nothing to do
	}

	// get next item, if at the end, restore the edit buffer
	++_hist_it;
	if (_hist_it == _hist_list.end()) {
		_buf = _hist_new;
	} else {
		_buf = *_hist_it;
	}

	// move cursor to the end of line
	_pos = _buf.size();
}

//! @brief
void
CmdDialog::histPrev(void)
{
	if (_hist_it == _hist_list.begin()) {
		return; // nothing to do
	}

	// save item so we can restore the edit buffer later
	if (_hist_it == _hist_list.end()) {
		_hist_new = _buf;
	}

	// get prev item
	_buf = *(--_hist_it);

	// move cursor to the end of line
	_pos = _buf.size();
}


syntax highlighted by Code2HTML, v. 0.9.1