/*
    PIP - Platform Independent Primitives
    Various tiles for PIScreen
    Copyright (C) 2015  Ivan Pelipenko peri4ko@gmail.com

    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 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 General Public License for more details.

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

#include "piscreentiles.h"
#include "piscreendrawer.h"

/*
 * \class PIScreen
 * \brief Console output class
 * \details
 * \section PIScreen_sec0 Synopsis
 * This class provides output to console with automatic alignment and update.
 * It supports tabs, keyboard listening, formats and colors.
 * 
 * \section PIScreen_sec1 Layout
 * %PIScreen works with variable pointers. You should add your variables with
 * functions \a addVariable() which receives label name, pointer to variable
 * and optional column and format. Columns count is dynamically increased if
 * new column used. E.g. if you add variable to empty tab to column 3, columns
 * count will be increased to 3, but two firsts columns will be empty. Each column
 * filled from top to bottom, but you can add just string with function
 * \a addString() or add empty line with function \a addEmptyLine(). Layout scheme:
 * \image html piconsole_layout.png
 * 
 * \section PIScreen_sec2 Keyboard usage
 * %PIScreen should to be single in application. %PIScreen aggregate PIKbdListener
 * which grab keyboard and automatic switch tabs by theirs bind keys. If there is no
 * tab binded to pressed key external function "slot" will be called
 * 
 **/

using namespace PIScreenTypes;


TileSimple::TileSimple(const PIString & n): PIScreenTile(n) {
	alignment = Left;
}


TileSimple::TileSimple(const TileSimple::Row & r): PIScreenTile() {
	alignment = Left;
	content << r;
}


void TileSimple::sizeHint(int & w, int & h) const {
	w = h = 0;
	piForeachC (Row & r, content)
		w = piMaxi(w, r.first.size_s());
	h = content.size_s();
}


void TileSimple::drawEvent(PIScreenDrawer * d) {
	for (int i = 0; i < content.size_s(); ++i) {
		Row & r(content[i]);
		int rx = 0;
		switch (alignment) {
		case Left: rx = x; break;
		case Center: rx = x + (width - r.first.size_s()) / 2; break;
		case Right: rx = x + width - r.first.size_s(); break;
		};
		d->drawText(rx, y + i, r.first, (Color)r.second.color_char, (Color)r.second.color_back, r.second.flags);
	}
}




TileList::TileList(const PIString & n): PIScreenTile(n) {
	alignment = Left;
	focus_flags = CanHasFocus | NextByArrowsHorizontal | NextByTab;
	lhei = offset = cur = 0;
	selection_mode = NoSelection;
}


void TileList::sizeHint(int & w, int & h) const {
	w = h = 0;
	piForeachC (Row & r, content)
		w = piMaxi(w, r.first.size_s());
	h = 3;
}


void TileList::drawEvent(PIScreenDrawer * d) {
	vert_line = d->artChar(PIScreenDrawer::LineVertical);
	lhei = height - 2;
	//int osp = piMini(3, lhei / 4);
	int is = piClampi(offset, 0, piMaxi(0, content.size_s() - 1)), ie = piClampi(offset + lhei, 0, content.size_s());
	if (is > 0) d->drawText(x, y, PIString(" /\\ ").repeat(width / 4), Green, Default, Bold);
	//piCout << is << ie << offset << lhei << content.size_s();
	for (int i = is; i < ie; ++i) {
		Row & r(content[i]);
		bool sel = i == cur && has_focus;
		if (sel) {
			int cy = y + i - is + 1;
			d->drawLine(x, cy, x + width - 2, cy, ' ', Default, Blue);
		}
		int rx(0);
		switch (alignment) {
		case Left: rx = x; break;
		case Center: rx = x + (width - 1 - r.first.size_s()) / 2; break;
		case Right: rx = x + width - 1 - r.first.size_s(); break;
		};
		CharFlags cf = r.second.flags;
		Color cc = (Color)r.second.color_char;
		if (selected[i]) {
			cf |= Bold;
			cc = Yellow;
		}
		d->drawText(rx, y + i - is + 1, r.first, cc, sel ? Blue : Default, cf);
	}
	int cx = x + width - 1;
	d->drawLine(cx, y + 1, cx, y + height - 2, vert_line, Green);
	if (content.size_s() > 1)
		d->drawPixel(cx, y + piRound(float(cur) / (content.size_s() - 1) * (lhei - 1)) + 1, ' ', Green, Green);
	if (ie < content.size_s()) d->drawText(x, y + height - 1, PIString(" \\/ ").repeat(width / 4), Green, Default, Bold);
}


bool TileList::keyEvent(PIKbdListener::KeyEvent key) {
	lhei = height - 2;
	int oo(0), osp = piMini(3, lhei / 4);
	switch (key.key) {
	case PIKbdListener::PageUp:
		cur -= lhei / 2;
		oo -= lhei / 2;
	case PIKbdListener::UpArrow:
		cur--;
		oo--;
		if (key.modifiers[PIKbdListener::Ctrl]) {
			cur -= 4;
			oo -= 4;
		}
		if (cur < 0) cur = 0;
		if (cur - offset < osp) offset += oo;
		if (offset < 0) offset = 0;
		return true;
	case PIKbdListener::Space:
		if (cur < 0 || cur >= content.size_s()) return true;
		switch (selection_mode) {
		case NoSelection: return false;
		case SingleSelection:
			if (selected.isEmpty()) selected << cur;
			else {
				bool add = !selected[cur];
				selected.clear();
				if (add) selected << cur;
			}
			raiseEvent(TileEvent(SelectionChanged));
			return true;
		case MultiSelection:
			if (selected[cur]) selected.remove(cur);
			else selected << cur;
			raiseEvent(TileEvent(SelectionChanged));
			break;
		}
	case PIKbdListener::PageDown:
		if (key.key == PIKbdListener::PageDown) {
			cur += lhei / 2;
			oo += lhei / 2;
		}
	case PIKbdListener::DownArrow:
		cur++;
		oo++;
		if (key.modifiers[PIKbdListener::Ctrl]) {
			cur += 4;
			oo += 4;
		}
		if (cur >= content.size_s()) cur = content.size_s() - 1;
		if (cur - offset >= lhei - osp) offset += oo;
		if (offset >= content.size_s() - lhei) offset = content.size_s() - lhei;
		if (offset < 0) offset = 0;
		return true;
	case PIKbdListener::Home:
		cur = offset = 0;
		return true;
	case PIKbdListener::End:
		cur = content.size_s() - 1;
		offset = content.size_s() - lhei;
		if (offset < 0) offset = 0;
		return true;
	case PIKbdListener::Return:
		if (cur >= 0 && cur < content.size_s())
			raiseEvent(TileEvent(RowPressed, cur));
		return true;
	case '*':
		if (selection_mode == TileList::MultiSelection) {
			PISet<int> nsel;
			for (int i = 0; i < content.size_s(); ++i)
				if (!selected[i]) nsel << i;
			selected = nsel;
		}
		raiseEvent(TileEvent(SelectionChanged));
		return true;
	case 'A':
		if (selection_mode == TileList::MultiSelection) {
			selected.clear();
			for (int i = 0; i < content.size_s(); ++i)
				selected << i;
		}
		raiseEvent(TileEvent(SelectionChanged));
		return true;
	}
	return PIScreenTile::keyEvent(key);
}




TileButton::TileButton(const PIString & n): PIScreenTile(n) {
	focus_flags = CanHasFocus | NextByTab | NextByArrowsAll;
}


void TileButton::sizeHint(int & w, int & h) const {
	w = text.size_s() + 2;
	h = 1;
}


void TileButton::drawEvent(PIScreenDrawer * d) {
	Color cb = has_focus ? Blue : Cyan;
	Color ct = has_focus ? White : Black;
	int ff = has_focus ? Bold : 0;
	d->fillRect(x, y, x + width, y + 1, ' ', Default, cb);
	d->drawText(x, y, "[", ct, Transparent, ff);
	d->drawText(x + (width - text.size_s()) / 2, y, text, ct, Transparent, ff);
	d->drawText(x + width - 1, y, "]", ct, Transparent, ff);
}


bool TileButton::keyEvent(PIKbdListener::KeyEvent key) {
	if (key.key == PIKbdListener::Space || key.key == PIKbdListener::Return) {
		raiseEvent(TileEvent(ButtonClicked));
		return true;
	}
	return PIScreenTile::keyEvent(key);
}




TileButtons::TileButtons(const PIString & n): PIScreenTile(n) {
	focus_flags = CanHasFocus | NextByTab;
	direction = Horizontal;
	alignment = PIScreenTypes::Center;
	cur = 0;
}


void TileButtons::sizeHint(int & w, int & h) const {
	w = h = 0;
	if (direction == Horizontal) {
		piForeachC (Button & b, content)
			w += b.first.size_s() + 4;
		w += piMaxi(0, content.size_s() - 1) * 2;
		h += 1;
	} else {
		piForeachC (Button & b, content)
			w = piMaxi(w, b.first.size_s() + 4);
		h += content.size_s();
		h += piMaxi(0, content.size_s() - 1);
	}
}


void TileButtons::drawEvent(PIScreenDrawer * d) {
	int cx = x, cy = y, shw, shh;
	sizeHint(shw, shh);
	int dx = 0;
	switch (alignment) {
	case PIScreenTypes::Center: dx = (width - shw) / 2; break;
	case PIScreenTypes::Right: dx = width - shw; break;
	default: break;
	}
	if (direction == PIScreenTypes::Horizontal)
		cx += dx;
	for (int i = 0; i < content.size_s(); ++i) {
		Color cb = Cyan;
		Color ct = Black;
		int ff = 0;
		if (i == cur && has_focus) {
			cb = Blue;
			ct = White;
			ff = Bold;
		}
		Button & b(content[i]);
		int cw = b.first.size_s() + 2, xo(0);
		if (direction == Vertical) {
			cw = width - 2;
			xo = (cw - b.first.size_s()) / 2 - 1;
		}
		d->fillRect(cx, cy, cx + cw + 2, cy + 1, ' ', Default, cb);
		d->drawText(cx, cy, "[", ct, Transparent, ff);
		d->drawText(cx + xo + 2, cy, b.first, ct, Transparent, ff);
		d->drawText(cx + cw + 1, cy, "]", ct, Transparent, ff);
		if (direction == Horizontal)
			cx += b.first.size_s() + 6;
		else
			cy += 2;
	}
}


bool TileButtons::keyEvent(PIKbdListener::KeyEvent key) {
	switch (key.key) {
	case PIKbdListener::LeftArrow:
	case PIKbdListener::UpArrow:
		cur--;
		if (cur < 0) cur = 0;
		return true;
	case PIKbdListener::RightArrow:
	case PIKbdListener::DownArrow:
		cur++;
		if (cur >= content.size_s()) cur = content.size_s() - 1;
		return true;
	case PIKbdListener::Space:
	case PIKbdListener::Return:
		raiseEvent(TileEvent(ButtonSelected, cur));
		return true;
	};
	return PIScreenTile::keyEvent(key);
}




TileCheck::TileCheck(const PIString & n): PIScreenTile(n) {
	focus_flags = CanHasFocus | NextByTab | NextByArrowsAll;
	toggled = false;
}


void TileCheck::sizeHint(int & w, int & h) const {
	w = text.size_s() + 4;
	h = 1;
}


void TileCheck::drawEvent(PIScreenDrawer * d) {
	Color cb = has_focus ? Blue : Cyan;
	Color ct = has_focus ? White : Black;
	int ff = has_focus ? Bold : 0;
	PIString cs("[ ]");
	if (toggled) cs[1] = '*';
	d->fillRect(x, y, x + width, y + 1, ' ', Default, cb);
	d->drawText(x, y, cs, ct, Transparent, ff);
	d->drawText(x + 4, y, text, ct, Transparent, ff);
}


bool TileCheck::keyEvent(PIKbdListener::KeyEvent key) {
	if (key.key == PIKbdListener::Space || key.key == PIKbdListener::Return) {
		toggled = !toggled;
		raiseEvent(TileEvent(Toggled, toggled));
		return true;
	}
	return PIScreenTile::keyEvent(key);
}




TileProgress::TileProgress(const PIString & n): PIScreenTile(n) {
	maximum = 100.;
	value = 0.;
	suffix = " %";
}


void TileProgress::sizeHint(int & w, int & h) const {
	w = 20;
	h = 1;
}


void TileProgress::drawEvent(PIScreenDrawer * d) {
	int v = maximum == 0. ? 0 : piClampd(piRoundd(value / maximum * 100.), 0, 100);
	PIString s = prefix + PIString::fromNumber(piRoundd(value)) + suffix;
	int w = piRoundd(v / 100. * width), sx = (width - s.size_s()) / 2;
	d->fillRect(x, y, x + width, y + 1, ' ', Default, Cyan);
	d->fillRect(x, y, x + w, y + 1, ' ', Default, Blue);
	if (w < sx)
		d->drawText(x + sx, y, s, Black, Transparent);
	else if (w >= sx + s.size_s())
		d->drawText(x + sx, y, s, White, Transparent);
	else {
		int fw = w - sx;
		d->drawText(x + sx, y, s.left(fw), White, Transparent);
		d->drawText(x + sx + fw, y, s.cutLeft(fw), Black, Transparent);
	}
}




TilePICout::TilePICout(const PIString & n): TileList(n) {
	max_lines = 1024;
	selection_mode = TileList::SingleSelection;
	PICout::setBufferActive(true);
}


void TilePICout::drawEvent(PIScreenDrawer * d) {
	PIString out = PICout::buffer(true);
	if (!out.isEmpty()) {
		PIStringList l = out.split("\n");
		bool scroll = (cur == content.size_s() - 1) || !has_focus;
		piForeachC (PIString & s, l)
			content << TileList::Row(s, format);
		if (content.size_s() > max_lines)
			content.remove(0, content.size_s() - max_lines);
		if (scroll) {
			offset = piMaxi(0, content.size_s() - lhei);
			cur = content.size_s() - 1;
		}
	}
	TileList::drawEvent(d);
}


bool TilePICout::keyEvent(PIKbdListener::KeyEvent key) {
	if (key.key == 'C') {
		content.clear();
		cur = offset = 0;
		return true;
	}
	return TileList::keyEvent(key);
}




TileInput::TileInput(const PIString & n): PIScreenTile(n) {
	focus_flags = CanHasFocus | NextByTab;
	back_format.color_back = White;
	format.color_char = Black;
	format.color_back = White;
	max_length = 1024;
	cur = offset = 0;
	inv = false;
}


void TileInput::sizeHint(int & w, int & h) const {
	w = 32;
	h = 1;
}


void TileInput::drawEvent(PIScreenDrawer * d) {
	PIString ps = text.mid(offset, width - 2);
	d->drawText(x + 1, y, ps, (Color)format.color_char, Transparent, (CharFlags)format.flags);
	if (offset > 0)
		d->drawText(x, y, "<", Green, Black, Bold);
	if (text.size_s() - offset >= width - 2)
		d->drawText(x + width - 1, y, ">", Green, Black, Bold);
	if (!has_focus) return;
	Color cb = (Color)format.color_char, cc = (Color)format.color_back;
	if (tm_blink.elapsed_m() >= 650) {
		tm_blink.reset();
		inv = !inv;
	}
	if (inv) piSwap(cb, cc);
	d->drawText(x + 1 + cur - offset, y, text.mid(cur, 1).expandLeftTo(1, ' '), cc, cb, (CharFlags)format.flags);
}


bool TileInput::keyEvent(PIKbdListener::KeyEvent key) {
	int lwid = piMaxi(0, width - 2);
	int oo(0), osp = piMini(3, lwid / 4);
	lwid--;
	switch (key.key) {
	case PIKbdListener::LeftArrow:
		cur--;
		oo--;
		if (key.modifiers[PIKbdListener::Ctrl]) {
			cur -= 4;
			oo -= 4;
		}
		if (cur < 0) cur = 0;
		if (cur - offset < osp) offset += oo;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::RightArrow:
		cur++;
		oo++;
		if (key.modifiers[PIKbdListener::Ctrl]) {
			cur += 4;
			oo += 4;
		}
		if (cur > text.size_s()) cur = text.size_s();
		if (cur - offset >= lwid - osp) offset += oo;
		if (offset >= text.size_s() - lwid) offset = text.size_s() - lwid;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::Home:
		cur = offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::End:
		cur = text.size_s();
		offset = text.size_s() - lwid;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::Backspace:
		if (cur > text.size_s() || text.isEmpty())
			return true;
		text.remove(cur - 1, 1);
		cur--;
		if (cur > text.size_s()) cur = text.size_s();
		if (cur - offset >= lwid - osp) offset += oo;
		if (offset >= text.size_s() - lwid) offset = text.size_s() - lwid;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::Delete:
		if (cur >= text.size_s() || text.isEmpty())
			return true;
		text.remove(cur, 1);
		if (cur < 0) cur = 0;
		if (cur > text.size_s()) cur = text.size_s();
		if (cur - offset < osp) offset += oo;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	case PIKbdListener::UpArrow:
	case PIKbdListener::DownArrow:
	case PIKbdListener::PageUp:
	case PIKbdListener::PageDown:
	case PIKbdListener::Insert:
	case PIKbdListener::Return:
	case PIKbdListener::Esc:
	case PIKbdListener::F1:
	case PIKbdListener::F2:
	case PIKbdListener::F3:
	case PIKbdListener::F4:
	case PIKbdListener::F5:
	case PIKbdListener::F6:
	case PIKbdListener::F7:
	case PIKbdListener::F8:
	case PIKbdListener::F9:
	case PIKbdListener::F10:
	case PIKbdListener::F11:
	case PIKbdListener::F12:
		break;
	default:
		text.insert(cur, PIChar(key.key));
		cur++;
		oo++;
		if (cur - offset >= lwid - osp) offset += oo;
		if (offset >= text.size_s() - lwid) offset = text.size_s() - lwid;
		if (offset < 0) offset = 0;
		reserCursor();
		return true;
	}
	return PIScreenTile::keyEvent(key);
}


void TileInput::reserCursor() {
	tm_blink.reset();
	inv = false;
}
