/*
    PIP - Platform Independent Primitives
    Various tiles for PIScreen
    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 "piscreentiles.h"

#include "piscreendrawer.h"


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;
	for (const auto & 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);
	}
}


// TileScrollBar

TileScrollBar::TileScrollBar(const PIString & n) {
	direction = Vertical;
	thickness = 1;
	minimum_ = value_ = 0;
	maximum_          = 100;
}


void TileScrollBar::setMinimum(int v) {
	minimum_ = v;
	_check();
}


void TileScrollBar::setMaximum(int v) {
	maximum_ = v;
	_check();
}


void TileScrollBar::setValue(int v) {
	value_ = v;
	_check();
}


void TileScrollBar::_check() {
	value_ = piClampi(value_, minimum_, maximum_);
}


void TileScrollBar::sizeHint(int & w, int & h) const {
	w = h = 0;
	if (direction == Vertical) {
		w = thickness;
		h = 255;
	} else {
		h = thickness;
		w = 255;
	}
}


void TileScrollBar::drawEvent(PIScreenDrawer * d) {
	line_char = d->artChar(direction == Vertical ? PIScreenDrawer::LineVertical : PIScreenDrawer::LineHorizontal);
	d->fillRect(x_, y_, x_ + width_, y_ + height_, line_char, Green);
	if (value_ >= minimum_ && value_ <= maximum_) {
		if (direction == Vertical) {
			int c = piRoundf((float(value_ - minimum_) / (maximum_ - minimum_)) * (height_ - 1));
			d->drawLine(x_, y_ + c, x_ + width_ - 1, y_ + c, ' ', Green, Green);
		} else {
			int c = piRoundf((float(value_ - minimum_) / (maximum_ - minimum_)) * (width_ - 1));
			d->drawLine(x_ + c, y_, x_ + c, y_ + height_ - 1, ' ', Green, Green);
		}
	}
}


bool TileScrollBar::mouseEvent(PIKbdListener::MouseEvent me) {
	return true;
}


// TileList

TileList::TileList(const PIString & n, SelectionMode sm): PIScreenTile(n) {
	alignment   = Left;
	focus_flags = CanHasFocus | NextByArrowsHorizontal | NextByTab | FocusOnMouseOrWheel;
	lhei = offset = cur = 0;
	mouse_sel           = false;
	selection_mode      = sm;
	scroll              = new TileScrollBar();
	scroll->size_policy = Ignore;
	addTile(scroll);
}


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


void TileList::resizeEvent(int w, int h) {
	scroll->x_      = x_ + width_ - 1;
	scroll->y_      = y_;
	scroll->width_  = 1;
	scroll->height_ = height_;
}


void TileList::drawEvent(PIScreenDrawer * d) {
	lhei   = height_ - 2;
	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);
	if (ie < content.size_s()) d->drawText(x_, y_ + height_ - 1, 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);
	}
	scroll->setMaximum(piMaxi(0, content.size_s() - 1));
	scroll->setValue(cur);
}


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);
}


bool TileList::mouseEvent(PIKbdListener::MouseEvent me) {
	if (me.action == PIKbdListener::MouseButtonRelease) return true;
	int mp = me.y - y() - 1 + offset;
	if (mp < 0 || mp >= content.size_s()) return true;
	cur = mp;
	switch (me.action) {
	case PIKbdListener::MouseButtonPress: mouse_sel = !selected.contains(cur); break;
	case PIKbdListener::MouseButtonDblClick: keyEvent(PIKbdListener::KeyEvent(PIKbdListener::Return)); return true;
	default: break;
	}
	if (me.buttons[PIKbdListener::MouseRight]) {
		switch (selection_mode) {
		case SingleSelection:
			selected.clear();
			selected << cur;
			raiseEvent(TileEvent(SelectionChanged));
			break;
		case MultiSelection:
			if (mouse_sel)
				selected << cur;
			else
				selected.remove(cur);
			raiseEvent(TileEvent(SelectionChanged));
			break;
		default: break;
		}
	}
	return true;
}


bool TileList::wheelEvent(PIKbdListener::WheelEvent we) {
	keyEvent(PIKbdListener::KeyEvent(we.direction ? PIKbdListener::PageUp : PIKbdListener::PageDown));
	return true;
}


// TileButton

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


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);
}


bool TileButton::mouseEvent(PIKbdListener::MouseEvent me) {
	if (me.action != PIKbdListener::MouseButtonRelease) return true;
	keyEvent(PIKbdListener::KeyEvent(PIKbdListener::Return));
	return true;
}


// TileButtons

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


void TileButtons::sizeHint(int & w, int & h) const {
	w = h = 0;
	if (direction == Horizontal) {
		for (const auto & b: content)
			w += b.first.size_s() + 4;
		w += piMaxi(0, content.size_s() - 1) * 2;
		h += 1;
	} else {
		for (const auto & 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);
	btn_rects.resize(content.size());
	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;
		}
		btn_rects[i] = Rect(cx, cy, cx + cw + 2, cy + 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);
}


bool TileButtons::mouseEvent(PIKbdListener::MouseEvent me) {
	if (me.action == PIKbdListener::MouseMove || me.action == PIKbdListener::MouseButtonPress) {
		for (int i = 0; i < btn_rects.size_s(); ++i)
			if (me.x >= btn_rects[i].x0 && me.x < btn_rects[i].x1 && me.y >= btn_rects[i].y0 && me.y < btn_rects[i].y1) {
				cur = i;
				break;
			}
		return true;
	}
	if (me.action != PIKbdListener::MouseButtonRelease) return true;
	keyEvent(PIKbdListener::KeyEvent(PIKbdListener::Return));
	return true;
}


// TileCheck

TileCheck::TileCheck(const PIString & n): PIScreenTile(n) {
	focus_flags = CanHasFocus | NextByTab | NextByArrowsAll | FocusOnMouse;
	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);
}


bool TileCheck::mouseEvent(PIKbdListener::MouseEvent me) {
	if (me.action == PIKbdListener::MouseButtonPress) {
		keyEvent(PIKbdListener::KeyEvent(PIKbdListener::Return));
		return true;
	}
	return false;
}


// TileProgress

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::TilePICout(const PIString & n): TileList(n) {
	max_lines      = 1024;
	selection_mode = TileList::SingleSelection;
	PICout::setOutputDevices(PICout::Buffer);
}


void TilePICout::drawEvent(PIScreenDrawer * d) {
	PIString out = PICout::getBufferAndClear();
	if (!out.isEmpty()) {
		PIStringList l = out.split("\n");
		bool scroll    = (cur == content.size_s() - 1) || !has_focus;
		for (const auto & s: l)
			content << TileList::Row(s.trimmed(), 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::TileInput(const PIString & n): PIScreenTile(n) {
	focus_flags            = CanHasFocus | NextByTab | FocusOnMouse;
	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((ushort)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;
}
