/*
 * This file is part of the Simutrans project under the Artistic License.
 * (see LICENSE.txt)
 */

/*
 * Defines a drop-down list with left/right arrows
 */

#include <string.h>

#include "../../macros.h"
#include "../../simdebug.h"
#include "../gui_frame.h"
#include "gui_combobox.h"
#include "../../simevent.h"
#include "../../display/simgraph.h"
#include "../../display/scr_coord.h"
#include "../../simcolor.h"
#include "../simwin.h"
#include "../../dataobj/loadsave.h"
#include "../../utils/simstring.h"
#include "../../dataobj/translator.h"


gui_combobox_t::gui_combobox_t(gui_scrolled_list_t::item_compare_func cmp) :
	gui_component_t(true),
	droplist(gui_scrolled_list_t::listskin, cmp)
{
	minimize = false;
	bt_prev.set_typ(button_t::arrowleft);
	bt_prev.set_pos(scr_coord(0, 2));

	bt_next.set_typ(button_t::arrowright);

	set_focusable(true); // needed, otherwise fails on closing when clicking elsewhere!

	search_str[0] = 0;
	old_searchstr[0] = 0;
	editstr[0] = 0;
	old_editstr[0] = 0;
	textinp.add_listener(this);

	first_call = true;
	wrapping = true;
	force_selection = false;
	allow_search = true;
	forbid_search = false;
	selection_when_open = -1;
	droplist.set_visible(false);
	droplist.add_listener(this);
	closed_size = get_size();
	max_size = scr_size(0,10*LINESPACE);
	opened_above = false;
	last_draw_offset = scr_coord(0,0);
}


/**
 * Events are notified to GUI components via this method
 */
bool gui_combobox_t::infowin_event(const event_t *ev)
{
	if( !(bt_next.enabled() || bt_prev.enabled()) ) {
		// no infowind events if disabled
		if(  droplist.is_visible()  ) {
			close_box();
		}
		return false;
	}

	if(  !droplist.is_visible()  ) {
DBG_MESSAGE("event","%d,%d",ev->click_pos.x, ev->click_pos.y);
		if(  bt_prev.getroffen(ev->click_pos)  ) {
DBG_MESSAGE("event","HOWDY!");
			bt_prev.pressed = IS_LEFT_BUTTON_PRESSED(ev);
			if(IS_LEFTRELEASE(ev)) {
				value_t p;
				bt_prev.pressed = false;
				set_selection( droplist.get_selection() > 0 ? droplist.get_selection() - 1 : wrapping ? droplist.get_count() - 1 : 0 );
				p.i = droplist.get_selection();
				call_listeners( p );
			}
			return true;
		}
		else if(  bt_next.getroffen(ev->click_pos)  ) {
			bt_next.pressed = IS_LEFT_BUTTON_PRESSED(ev);
			if(IS_LEFTRELEASE(ev)) {
				bt_next.pressed = false;
				value_t p;
				set_selection( droplist.get_selection() < (sint32)droplist.get_count() - 1 ? droplist.get_selection() + 1 : wrapping ? 0 : droplist.get_count() - 1 );
				p.i = droplist.get_selection();
				call_listeners(p);
			}
			return true;
		}
	}
	else if(  (IS_WHEELUP(ev)  ||  IS_WHEELDOWN(ev))  &&  droplist.getroffen(ev->mouse_pos)  ) {
		// scroll the list
		event_t ev2 = *ev;
		ev2.move_origin(droplist.get_pos());

		droplist.infowin_event(&ev2);
		return true;
	}

	// goto next/previous choice
	if(  IS_KEYDOWN(ev)  &&  (ev->ev_code == SIM_KEYCODE_UP  ||  ev->ev_code == SIM_KEYCODE_DOWN)  &&  droplist.get_count()>0) {
		int delta = ev->ev_code == SIM_KEYCODE_UP ? -1 : 1;
		int sel = droplist.get_selection();
		if (delta < 0 && sel < 0) {
			// start at last
			sel = 0;
		}
		const int cnt = droplist.get_count();
		for (uint32 i = 0; i < droplist.get_count(); i++) {
			sel = (sel + delta + cnt) % cnt;
			if (droplist.get_element(sel)->is_visible()) {
				break;
			}
		}
		if (sel != droplist.get_selection()) {
			droplist.set_selection(sel);
			if (!droplist.is_visible()) {
				// update if we are not in the open list scrolling
				value_t p;
				p.i = droplist.get_selection();
				this->call_listeners(p);
				textinp.set_text(NULL,0),
				reset_selected_item_name();
			}
			else if (search_str[0] == 0) {
				droplist.show_selection(sel);
				droplist.set_selection(sel);
				reset_selected_item_name();
			}
		}
		return true;
	}

	if(  IS_KEYDOWN(ev)  &&  (ev->ev_code == SIM_KEYCODE_ENTER  ||  ev->ev_code == SIM_KEYCODE_TAB)  &&  droplist.is_visible()  ) {
		// close with enter/tab
		close_box();
		return ev->ev_code == SIM_KEYCODE_ENTER;
	}

	if(  IS_LEFTCLICK(ev)  ||  IS_LEFTDRAG(ev)  ||  IS_LEFTRELEASE(ev)  ||  IS_LEFTDRAG(ev)  ) {

		if(first_call) {

			// ignore clicks outside if closed
			scr_rect this_comp( get_size() );
			if(  !droplist.is_visible()  &&  !this_comp.contains(ev->click_pos)  ) {
				// not us, just in old focus from previous selection or tab
				return false;
			}

			// swallow the first mouse click
			if(  !IS_LEFTRELEASE(ev)  ) {
				return false;
			}
			first_call = false;

			// else prepare for selection (after a left mbutton release event!)
			droplist.set_visible(true);
			selection_when_open = droplist.get_selection();

			// determine possible size of droplist and whether open below/above input field
			scr_coord_val win_height = win_get_top()->get_windowsize().h - D_TITLEBAR_HEIGHT;
			scr_coord_val last_draw_y = last_draw_offset.y  + get_pos().y - win_get_pos(win_get_top()).y- D_TITLEBAR_HEIGHT;
			scr_coord_val height_above = last_draw_y - D_V_SPACE;
			scr_coord_val height_below = win_height - (last_draw_y + textinp.get_size().h + D_V_SPACE);
			// we try to show all all the time
			scr_coord_val drop_max_height = droplist.get_max_size().h;
			scr_coord_val request_height = min(max(height_above,height_below), drop_max_height);

			// request size of droplist, should stay inside window
			// call returns actual height, might be smaller than request_height
			request_height = droplist.request_size(scr_size(this->size.w, min(request_height, max(height_above, height_below))) ).h;

			// open below if enough space or more space than above
			opened_above = request_height > height_below  &&  height_below < height_above;
			set_pos(get_pos());

			int sel = droplist.get_selection();
			if((uint32)sel>=(uint32)droplist.get_count()  ||  !droplist.get_element(sel)->is_valid()) {
				sel = 0;
			}
			droplist.show_selection(sel);
			search_str[0] = 0;
			old_searchstr[0] = 0;
			if (!forbid_search) {
				allow_search = drop_max_height > request_height;
			}

			if(allow_search) {
				textinp.set_text(NULL, 0);
				textinp.set_text(search_str, lengthof(search_str));
				textinp.set_placeholder_text(translator::translate("Type to search..."));
			}
		}
		else if (droplist.is_visible()) {

			if( droplist.getroffen(ev->click_pos)  ) {
				event_t ev2 = *ev;
				ev2.move_origin(droplist.get_pos());
				if(  droplist.infowin_event(&ev2)  ) {
					if(IS_LEFTRELEASE(ev)) {
						close_box();
					}
				}
			}
			else {
				// acting on "release" is better than checking for "new selection"
				if(  IS_LEFTRELEASE(ev)  ) {
					close_box();
				}
			}
		}
		return true;
	}
	else if(ev->ev_class==INFOWIN  &&  (ev->ev_code==WIN_CLOSE  ||  ev->ev_code==WIN_UNTOP)  ) {
DBG_MESSAGE("gui_combobox_t::infowin_event()","close");
		textinp.infowin_event(ev);
		return true;
	}
	else {
		// finally handle textinput
		gui_scrolled_list_t::scrollitem_t *item = droplist.get_selected_item();
		if (droplist.is_visible() && allow_search) {
			// we are searching, not renaming
			sint32 first_match = -1;
			event_t ev2 = *ev;
			ev2.move_origin(textinp.get_pos());
			if (textinp.infowin_event(&ev2)) {
				if (strcmp(search_str,old_searchstr)!=0) {
					for (sint32 i = 0; i < (sint32)droplist.get_count(); i++) {
						droplist.get_element(i)->set_visible(STRICMP(droplist.get_element(i)->get_text(), search_str));
						if (droplist.get_element(i)->is_visible() && first_match <= droplist.get_selection()) {
							first_match = i;
						}
					}
					droplist.show_selection(first_match);
					droplist.set_selection(first_match);
					strcpy(old_searchstr, search_str);
				}
				return true;
			}
		}
		else if(  item==NULL  ||  item->is_editable()  ) {
			event_t ev2 = *ev;
			ev2.move_origin(textinp.get_pos());
			return textinp.infowin_event(&ev2);
		}
	}
	return false;
}


/* selection now handled via callback */
bool gui_combobox_t::action_triggered( gui_action_creator_t *comp,value_t p)
{
	if (  comp == &droplist  ) {
		const int selection = (int)p.i;
DBG_MESSAGE("gui_combobox_t::infowin_event()", "scroll selected %i", selection);
		set_selection(selection);
	}
	else if (  comp == &textinp  ) {
		rename_selected_item();
	}
	return false;
}


/**
 * Draw the component
 */
void gui_combobox_t::draw(scr_coord offset)
{
	last_draw_offset = offset;
	// text changed? Then update it
	gui_scrolled_list_t::scrollitem_t *item = droplist.get_selected_item();
	if (!droplist.is_visible()) {
		// maybe rename item unless we are searching reight now
		if (item && item->is_valid() && item->is_editable() && strncmp(item->get_text(), old_editstr, lengthof(old_editstr))) {
			reset_selected_item_name();
		}
	}

	offset += pos;

	bool with_focus = (win_get_focus()==this)  &&  (item==NULL  ||  item->is_editable());
	textinp.display_with_focus( offset, with_focus);

	if(  droplist.is_visible()  ) {
		droplist.draw(offset);
	}
	else {
		bt_prev.draw(offset);
		bt_next.draw(offset);
	}
}


void gui_combobox_t::enable()
{
	set_focusable(true);
	bt_next.enable();
	bt_prev.enable();
	textinp.set_color(SYSCOL_EDIT_TEXT);
}

void gui_combobox_t::disable()
{
	set_focusable(false);
	bt_next.disable();
	bt_prev.disable();
	textinp.set_color(SYSCOL_EDIT_TEXT_DISABLED);
}

/**
 * sets the selection
 */
void gui_combobox_t::set_selection(int s)
{
	search_str[0] = 0;

	// try to finish renaming first
	rename_selected_item();

	// just set it
	if (force_selection  &&  s < 0  &&  droplist.get_count() > 0) {
		// force always a valid selection unless empty
		s = 0;
	}
	droplist.set_selection(s);
	if (droplist.is_visible()) {
		// change also offset of scrollbar
		droplist.show_selection( s );
	}

	// edit the text
	reset_selected_item_name();
}


/**
 * Check whether we should rename selected item
 */
void gui_combobox_t::rename_selected_item()
{
	gui_scrolled_list_t::scrollitem_t *item = droplist.get_selected_item();
	// if name was not changed in the meantime, we can rename it
	if(  item  &&  item->is_valid()  &&  item->is_editable()  ) {
		const char *current_str = ((gui_scrolled_list_t::const_text_scrollitem_t *)item)->get_text();
		if(  strncmp( current_str, old_editstr, lengthof(old_editstr)-1) == 0  &&  strncmp( current_str, editstr, lengthof(old_editstr)-1) != 0  ) {
			item->set_text(editstr);
		}
	}
}


void gui_combobox_t::reset_selected_item_name()
{
	gui_scrolled_list_t::scrollitem_t *item = droplist.get_selected_item();
	if(  item==NULL  ) {
		editstr[0] = 0;
		if(!droplist.is_visible()) {
			textinp.set_text( editstr, 0  );
		}
		droplist.set_selection(-1);
	}
	else if(  item->is_valid()  ) {
		const char *current_str = ((gui_scrolled_list_t::const_text_scrollitem_t *)item)->get_text();
		if(  strncmp( current_str, old_editstr, lengthof(old_editstr)-1 )!=0  ) {
			tstrncpy( editstr, current_str, lengthof(editstr) );
			if (!droplist.is_visible()) {
				textinp.set_text(editstr, lengthof(editstr));
			}
		}
	}
	textinp.set_color(SYSCOL_EDIT_TEXT);
	tstrncpy( old_editstr, editstr, lengthof(old_editstr) );
}


/**
* Release the focus if we had it
*/
void gui_combobox_t::close_box()
{
	bool selection_changed = selection_when_open != droplist.get_selection();
	selection_when_open = droplist.get_selection();

	reset_selected_item_name();
	if(selection_changed) {
		value_t p;
		p.i = droplist.get_selection();
		call_listeners(p);
	}
	if (allow_search && (search_str[0] || selection_changed)) {
		// reset filter: set all visible again
		search_str[0] = 0;
		for (uint32 i = 0; i < droplist.get_count(); i++) {
			droplist.get_element(i)->set_visible(true);
		}
		textinp.set_text(NULL, 0);  // also to reset cursor position
		textinp.set_placeholder_text(NULL);
	}

	textinp.set_text(editstr, lengthof(editstr) );
	if (!selection_changed) {
		sint32 len = strlen(editstr);
		textinp.set_cursor(len, len);
	}
	droplist.set_visible(false);
	first_call = true;
}


void gui_combobox_t::set_pos(scr_coord pos_par)
{
	gui_component_t::set_pos( pos_par );

	if(  opened_above  ) {
		droplist.set_pos( scr_size( (get_size().w -droplist.get_size().w)/2, D_V_SPACE/4 - droplist.get_size().h) );
	}
	else {
		droplist.set_pos( scr_size( (get_size().w -droplist.get_size().w)/2, textinp.get_size().h) );
	}
}


void gui_combobox_t::set_size(scr_size size)
{
	closed_size = size;
	gui_component_t::set_size( size );

	droplist.request_size(scr_size(this->size.w, droplist.get_size().h));

	textinp.set_size( scr_size( size.w - bt_prev.get_size().w - bt_next.get_size().w - D_H_SPACE, closed_size.h ) );
	set_pos(get_pos());

	bt_prev.set_pos( scr_coord(0,(size.h-D_ARROW_LEFT_HEIGHT)/2) );
	textinp.align_to( &bt_prev, scr_coord( D_H_SPACE / 2, 0) );
	bt_next.align_to( &textinp, scr_coord( D_H_SPACE / 2, 0) );
}


/**
* set maximum size for control
*/
void gui_combobox_t::set_max_size(scr_size max)
{
	max_size = max;
	droplist.request_size( scr_size( size.w, max_size.h - closed_size.h ) );
}


scr_size gui_combobox_t::get_min_size() const
{
	scr_size bl = bt_prev.get_min_size();
	scr_size ti = textinp.get_min_size();
	scr_size sl = droplist.get_container_min_size();
	scr_size br = bt_next.get_min_size();

	if (sl.w == scr_size::inf.w) {
		// contains editable items, use min-width of input
		return scr_size(bl.w + ti.w + br.w + D_H_SPACE, max(max(bl.h, ti.h), br.h));
	}
	else {
		return scr_size(bl.w + br.w + sl.w - D_H_SPACE, max(max(bl.h, ti.h), br.h));
	}
}


scr_size gui_combobox_t::get_max_size() const
{
	scr_size msize = get_min_size();
	if (!minimize) {
		// expand as much as one can
		msize.w = droplist.get_max_size().w;
	}
	return msize;
}


void gui_combobox_t::rdwr( loadsave_t *file )
{
	// todo: all pending edits and searches are lost/executed
	sint32 i = get_selection();
	file->rdwr_long(i);
	set_selection(i);
}
