/*
XOWA: the XOWA Offline Wiki Application
Copyright (C) 2012 gnosygnu@gmail.com

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
package gplx.gfui.kits.swts; import gplx.*; import gplx.gfui.*; import gplx.gfui.kits.*;
import gplx.*;
import gplx.core.envs.Op_sys;
import gplx.core.envs.Op_sys_;
import gplx.core.threads.Thread_adp_;
import gplx.gfui.controls.gxws.GxwComboBox;
import gplx.gfui.controls.gxws.GxwElem;
import gplx.gfui.controls.standards.GfuiComboBox;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

public class Swt_combo_ctrl extends Swt_text_w_border implements GxwElem, GxwComboBox, Swt_control, Gfo_evt_mgr_owner {	// REF: https://www.eclipse.org/forums/index.php/t/351029/; http://git.eclipse.org/c/platform/eclipse.platform.swt.git/tree/examples/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet320.java
	private final Text swt_text;
	private final Swt_combo_list list;
	public Swt_combo_ctrl(Swt_control owner, Color color, Keyval_hash ctorArgs) {
		super(owner, color, new Keyval_hash(), Swt_text_w_border.Margin_t__combo);
		Display display = owner.Under_control().getDisplay();
		Shell shell = owner.Under_control().getShell();
		this.swt_text = super.Under_text();
		this.list = new Swt_combo_list(display, shell, this);

		swt_text.addListener(SWT.KeyDown, new Swt_combo_text__key_down(list));
		swt_text.addListener(SWT.MouseUp, new Swt_combo_text__mouse_up(list));

		Table swt_list = list.Under_table_as_swt();
		swt_list.addListener(SWT.DefaultSelection, new Swt_combo_list__default_selection(this, list));
		swt_list.addListener(SWT.KeyDown, new Swt_combo_list__default_selection(this, list));
		swt_list.addListener(SWT.MouseDown, new Swt_combo_list__mouse_down(this, list));

		// listeners to hide list-box when focus is transfered, shell is moved, text-box is clicked, etc 
		Swt_combo_ctrl__list_hider_cmd list_hide_cmd = new Swt_combo_ctrl__list_hider_cmd(list);
		Swt_combo_ctrl__focus_out focus_out_lnr = new Swt_combo_ctrl__focus_out(display, this, list, list_hide_cmd);
		swt_text.addListener(SWT.FocusOut, focus_out_lnr);
		swt_list.addListener(SWT.FocusOut, focus_out_lnr);
		swt_text.addListener(SWT.FocusIn, new Swt_combo_text__focus_in(this, list));
		swt_list.addListener(SWT.FocusIn, new Swt_combo_ctrl__focus_in(display, list, list_hide_cmd));		
		shell.addListener(SWT.Move, new Swt_combo_shell__move(list));
	}
	@Override public Gfo_evt_mgr Evt_mgr() {return ev_mgr;} private Gfo_evt_mgr ev_mgr; public void Evt_mgr_(Gfo_evt_mgr v) {ev_mgr = v;}
	@Override public Object SelectedItm() {return null;}
	@Override public void SelectedItm_set(Object v) {}
	@Override public void Sel_(int bgn, int end) 					{swt_text.setSelection(new Point(bgn, end));}
	@Override public String[] DataSource_as_str_ary() 				{return list.Items_str_ary();}
	@Override public void DataSource_set(Object... ary) 			{list.Items_((String[])ary);}
	@Override public String Text_fallback() 						{return text_fallback;} private String text_fallback = "";// preserve original-text when using cursor keys in list-box
	@Override public void Text_fallback_(String v) 					{text_fallback = v;}
	public void Text_fallback_restore() {
		if (String_.Len_eq_0(text_fallback)) return; // handle escape pressed after dropdown is visible, but down / up not pressed
		this.Text_(text_fallback);
		this.text_fallback = "";		
	}
	@Override public void Items__update(String[] ary) 				{list.Items_(ary);}
	@Override public void Items__size_to_fit(int count) 			{list.Resize_shell(count);}
	@Override public int List_sel_idx() 							{return list.Sel_idx();}
	@Override public void List_sel_idx_(int v) 						{list.Sel_idx_(v);}
	@Override public boolean List_visible() 						{return list.Visible();}
	@Override public void List_visible_(boolean v) 					{list.Visible_(v);}
	@Override public void Items__visible_rows_(int v) 				{list.Visible_rows = v;}
	@Override public void Items__jump_len_(int v) 					{list.Jump_len = v;}
	public Rectangle Bounds() {return super.Under_control().getBounds();}
	public String Text() {return swt_text.getText();} public void Text_(String v) {swt_text.setText(v);}
	public void Sel_all() {
		String text_text = swt_text.getText();
		this.Sel_(0, String_.Len(text_text));
	}
}
class Swt_combo_list {
	private final Display display; private final Shell owner_shell;
	private final Swt_combo_ctrl ctrl;
	private final Shell shell;
	private final Table table;
	private int list_len;
	public int Jump_len = 5;
	public int Visible_rows = 10;
	public Swt_combo_list(Display display, Shell owner_shell, Swt_combo_ctrl ctrl) {
		this.display = display; this.owner_shell = owner_shell; this.ctrl = ctrl;
		this.shell = new Shell(display, SWT.ON_TOP);
		shell.setLayout(new FillLayout());
		this.table = new Table(shell, SWT.SINGLE);
	}
	public Table Under_table_as_swt() {return table;}
	public int Items_len() {return table.getItemCount();}
	public TableItem[] Items() {return table.getItems();}
	public TableItem Sel_itm(int i) {return table.getSelection()[i];}
	public void Items_text_(int idx, String v) {table.getItem(idx).setText(v);}
	public String[] Items_str_ary() {return items_str_ary;} private String[] items_str_ary = String_.Ary_empty;
	public void Items_(String[] new_ary) {
		int new_len = new_ary.length;
		if (new_len == 0) Visible_(Bool_.N);	// if new_ary is empty, then hide list box; else, brief flicker as items are removed 

		// remove all cur-items that are no longer needed b/c new_ary is smaller
		int cur_len = list_len;
		for (int i = new_len; i < cur_len; ++i) {
			table.remove(new_len);
		}

		// update new_ary;
		for (int i = 0; i < new_len; ++i) {
			TableItem item = null;
			if (i < cur_len)	// existing item; reuse it
				item = table.getItem(i);
			else				// no ite; create a new one
				item = new TableItem(table, SWT.NONE);
			String cur_text = item.getText();
			String new_text = new_ary[i];
			if (!String_.Eq(cur_text, new_text) && new_text != null) {
				item.setText(new_text);
			}
		}
		this.list_len = new_len;
		
		// resize list-shell to # of items
		int max_len = this.Visible_rows;
		if (	new_len == cur_len						// do not resize if same #
			|| 	new_len > max_len && cur_len > max_len	// do not resize if new_len and cur_len are both off-screen
			) {}
		this.items_str_ary = new_ary;
//		else
//			Resize_shell(list_len);
	}
	public int Sel_idx() {return table.getSelectionIndex();}
	public void Sel_idx_(int v) {table.setSelection(v);}
	public void Sel_idx_nudge(boolean fwd) 	{Sel_idx_adj(fwd, 1);}
	public void Sel_idx_jump(boolean fwd) 	{Sel_idx_adj(fwd, Jump_len);}
	private void Sel_idx_adj(boolean fwd, int adj) {
		if (!Visible()) Visible_(Bool_.Y);	// these are called by cursor keys; always make visible
		int cur_idx = table.getSelectionIndex();
		int new_idx = cur_idx;
		int idx_n = list_len - 1;
		if (fwd) {
			if (cur_idx == idx_n)
				new_idx = -1;
			else if (cur_idx == -1)
				new_idx = 0;
			else {
				new_idx = cur_idx + adj;
				if (new_idx >= idx_n)
					new_idx = idx_n;
			}
		}
		else {
			if (cur_idx == 0)
				new_idx = -1;
			else if (cur_idx == -1)
				new_idx = idx_n;
			else {
				new_idx = cur_idx - adj;
				if (new_idx < 0)
					new_idx = 0;
			}
		}
		Sel_idx_by_key(new_idx);
	}
	public void Sel_idx_by_key(int v) {
		table.setSelection(v);
		if (v == -1) {	// nothing selected; restore orig
			ctrl.Text_fallback_restore();
		} else {		// something selected; transfer selected item to text
			String sel_text = table.getItem(v).getText(); 
			ctrl.Text_(sel_text);
			ctrl.Sel_(sel_text.length(), sel_text.length());
		}
		Gfo_evt_mgr_.Pub(ctrl, GfuiComboBox.Evt__selected_changed);
	} 
	public boolean Visible() {return shell.isVisible();}
	public void Visible_(boolean v) {
		if (v && list_len == 0) return;	// never show if 0 items; occurs when users presses alt+down or down when in combo-box 
		shell.setVisible(v);
	}
	public void Resize_shell(int len) {
		Rectangle text_bounds = display.map(owner_shell, null, ctrl.Bounds());
		int adj = Op_sys.Cur().Tid() == Op_sys.Lnx.Tid() ? 9 : 2;	// NOTE: magic-numbers from gnosygnu's Windows-7, OS-X and openSUSE; Linux # is not correct for different number of items, but looks acceptable for 25;
		int max_len = this.Visible_rows;
		int height = (table.getItemHeight() * (len > max_len ? max_len : len)) + adj;
		if (height != shell.getSize().y) // only resize if height is different
			shell.setBounds(text_bounds.x, text_bounds.y + text_bounds.height - 1, text_bounds.width, height);		
	}
}
class Swt_combo_text__key_down implements Listener {			// for list-box, highlight item or toggle visiblility based on keys
	private final Swt_combo_list list;
	public Swt_combo_text__key_down(Swt_combo_list list) {
		this.list = list;
	}
	@Override public void handleEvent(Event event) {
		try {
			int mask = event.stateMask;
			boolean no_modifier = false;
			boolean ctrl_modifier = false;
			boolean alt_modifier = false;
			if 		((mask & SWT.ALT) 	== SWT.ALT) {alt_modifier = true;}
			else if	((mask & SWT.CTRL)	== SWT.CTRL){ctrl_modifier = true;}
			else if ((mask & SWT.SHIFT)	== SWT.SHIFT){}
			else
				no_modifier = true;
			switch (event.keyCode) {
				case SWT.ARROW_DOWN:
					if (event.stateMask == SWT.ALT) {
						list.Visible_(true);
					} else {
						list.Sel_idx_nudge(Bool_.Y);
					}
					event.doit = false;
					break;
				case SWT.ARROW_UP:
					if (event.stateMask == SWT.ALT) {
						list.Visible_(false);
					} else {
						list.Sel_idx_nudge(Bool_.N);
					}
					event.doit = false;
					break;
				case SWT.PAGE_DOWN:
					if (no_modifier) {
						list.Sel_idx_jump(Bool_.Y);
						event.doit = false;
					}
					else if (ctrl_modifier || alt_modifier) {
						list.Visible_(Bool_.N);
						event.doit = false;
					}					
					break;
				case SWT.PAGE_UP:
					if (no_modifier) {
						list.Sel_idx_jump(Bool_.N);
						event.doit = false;
					}
					else if (ctrl_modifier || alt_modifier) {
						list.Visible_(Bool_.N);
						event.doit = false;
					}					
					break;
				case SWT.CR:
					if (list.Visible() && list.Sel_idx() != -1) {
						list.Visible_(false);
					}
					break;
				case SWT.ESC:
					if (list.Visible()) {	// NOTE: must check if list is visible, else will need to press escape twice to restore url; DATE:2017-02-15 
						list.Sel_idx_by_key(-1);
						list.Visible_(false);
					}
					break;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}
class Swt_combo_text__mouse_up implements Listener {			// if list-box is visible, left-click on text-box should hide list-box; note that left-click does not fire focus-in event; EX: focus html-box; click on url-box; focus-in event not fired
	private final Swt_combo_list list;
	public Swt_combo_text__mouse_up(Swt_combo_list list) {
		this.list = list;
	}
	@Override public void handleEvent(Event event) {
        if (event.button == 1) {	// left-click
        	if (list.Visible()) {
        		list.Visible_(false);
        	}
        }
	}
}
class Swt_combo_list__default_selection implements Listener {	// transfer list-box's selected to text-box
	private final Swt_combo_ctrl ctrl; private final Swt_combo_list list;
	public Swt_combo_list__default_selection(Swt_combo_ctrl ctrl, Swt_combo_list list) {
		this.ctrl = ctrl; this.list = list;
	}
	@Override public void handleEvent(Event arg0) {		
		ctrl.Text_(list.Sel_itm(0).getText());
		list.Visible_(false);
	}	
}
class Swt_combo_list__key_down implements Listener {			// hide list-box if escape pressed
	private final Swt_combo_ctrl ctrl; private final Swt_combo_list list;
	public Swt_combo_list__key_down(Swt_combo_ctrl ctrl, Swt_combo_list list) {
		this.ctrl = ctrl;
		this.list = list;
	}
	@Override public void handleEvent(Event event) {		
		if (event.keyCode == SWT.ESC) {
			ctrl.Text_fallback_restore();
			list.Visible_(false);
		}
	}
}
class Swt_combo_list__mouse_down implements Listener {			// left-click on list-box should transfer item to text-box and publish "accepted" event
	private final Swt_combo_ctrl ctrl;
	private final Swt_combo_list list;
	public Swt_combo_list__mouse_down(Swt_combo_ctrl ctrl, Swt_combo_list list) {
		this.ctrl = ctrl;
		this.list = list;
	}
	@Override public void handleEvent(Event event) {
		if (event.button == 1) {	// left-click
			ctrl.Text_(list.Sel_itm(0).getText());
			Gfo_evt_mgr_.Pub(ctrl, GfuiComboBox.Evt__selected_changed);
			Gfo_evt_mgr_.Pub(ctrl, GfuiComboBox.Evt__selected_accepted);
		}
	}
}
class Swt_combo_ctrl__focus_out implements Listener {			// run hide-cmd when list-box when text-box / list-box loses focus
	private final Display display;
	private final Control text_as_swt, list_as_swt;
	private final Swt_combo_ctrl__list_hider_cmd list_hide_cmd;
	public Swt_combo_ctrl__focus_out(Display display, Swt_combo_ctrl ctrl, Swt_combo_list list, Swt_combo_ctrl__list_hider_cmd list_hide_cmd) {
		this.list_hide_cmd = list_hide_cmd;
		this.display = display;
		this.text_as_swt = ctrl.Under_text();
		this.list_as_swt = list.Under_table_as_swt();
	}
	@Override public void handleEvent(Event arg0) {
		if (display.isDisposed()) return;
		Control control = display.getFocusControl();
		if (control == null || control == text_as_swt || control == list_as_swt) {
			list_hide_cmd.Active = true;
			display.asyncExec(list_hide_cmd);
		}
	}
}
class Swt_combo_ctrl__focus_in implements Listener {		// cancel hide-cmd if list-box gains focus
	private final Display display;
	private final Swt_combo_ctrl__list_hider_cmd list_hide_cmd;
	private final Control list_as_swt;
	
	public Swt_combo_ctrl__focus_in(Display display, Swt_combo_list list, Swt_combo_ctrl__list_hider_cmd list_hide_cmd) {
		this.display = display;
		this.list_as_swt = list.Under_table_as_swt();
		this.list_hide_cmd = list_hide_cmd;
	}
	@Override public void handleEvent(Event arg0) {
		if (display.isDisposed()) return;
		Control control = display.getFocusControl();
		if (control == list_as_swt)
			list_hide_cmd.Active = false;
	}
}
class Swt_combo_text__focus_in implements Listener {		// hide list-box when text-box is focused
	private final Swt_combo_ctrl ctrl; private final Swt_combo_list list;
	public Swt_combo_text__focus_in(Swt_combo_ctrl ctrl, Swt_combo_list list) {
		this.ctrl = ctrl; this.list = list;
	}
	@Override public void handleEvent(Event arg0) {
		if (list.Visible())
			list.Visible_(false);
		else
			ctrl.Sel_all();
	}
}
class Swt_combo_shell__move implements Listener {			// hide list-box when shell is moved
	private final Swt_combo_list list;
	public Swt_combo_shell__move(Swt_combo_list list) {this.list = list;}
	@Override public void handleEvent(Event arg0) {
		list.Visible_(false);
	}
}
class Swt_combo_ctrl__list_hider_cmd implements Runnable {	// hide list-box; can be "canceled"
	private final Swt_combo_list list;
	public boolean Active = true;
	public Swt_combo_ctrl__list_hider_cmd(Swt_combo_list list) {
		this.list = list;
	}
	@Override public void run() {
		if (Active)
			list.Visible_(false);
	}
}
