content: // This file is part of AceWiki.
  // Copyright 2008-2013, AceWiki developers.
  // 
  // AceWiki 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.
  // 
  // AceWiki 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 AceWiki. If
  // not, see http://www.gnu.org/licenses/.
  
  package ch.uzh.ifi.attempto.preditor;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import nextapp.echo.app.Alignment;
  import nextapp.echo.app.Border;
  import nextapp.echo.app.Color;
  import nextapp.echo.app.Column;
  import nextapp.echo.app.Extent;
  import nextapp.echo.app.Font;
  import nextapp.echo.app.Grid;
  import nextapp.echo.app.Insets;
  import nextapp.echo.app.Row;
  import nextapp.echo.app.event.ActionEvent;
  import nextapp.echo.app.event.ActionListener;
  import nextapp.echo.app.event.WindowPaneEvent;
  import nextapp.echo.app.event.WindowPaneListener;
  import nextapp.echo.app.layout.GridLayoutData;
  import ch.uzh.ifi.attempto.base.ConcreteOption;
  import ch.uzh.ifi.attempto.base.DefaultTextOperator;
  import ch.uzh.ifi.attempto.base.LoggerContext;
  import ch.uzh.ifi.attempto.base.NextTokenOptions;
  import ch.uzh.ifi.attempto.base.PredictiveParser;
  import ch.uzh.ifi.attempto.base.TextContainer;
  import ch.uzh.ifi.attempto.base.TextElement;
  import ch.uzh.ifi.attempto.base.TextOperator;
  import ch.uzh.ifi.attempto.echocomp.EchoThread;
  import ch.uzh.ifi.attempto.echocomp.GeneralButton;
  import ch.uzh.ifi.attempto.echocomp.Label;
  import ch.uzh.ifi.attempto.echocomp.LocaleResources;
  import ch.uzh.ifi.attempto.echocomp.Style;
  import ch.uzh.ifi.attempto.echocomp.TabSensitiveTextField;
  import ch.uzh.ifi.attempto.echocomp.TextField;
  import echopoint.DirectHtml;
  
  //import static ch.uzh.ifi.attempto.echocomp.KeyStrokes.*;
  
  /**
   * This class represents a predictive editor window. The predictive editor enables easy creation of
   * texts that comply with a certain grammar. The users can create such a text word-by-word by
   * clicking on one of different menu items. The menu items are structured into menu blocks each of
   * which has a name that is displayed above the menu block.
   * 
   * @author Tobias Kuhn
   */
  public class PreditorWindow extends nextapp.echo.app.WindowPane implements ActionListener, WindowPaneListener {
  	
  	private static final long serialVersionUID = -7815494421993305554L;
  	
  	static {
  		LocaleResources.loadBundle("ch/uzh/ifi/attempto/echocomp/text");
  		LocaleResources.loadBundle("ch/uzh/ifi/attempto/preditor/text");
  	}
  	
  	private final TextContainer textContainer = new TextContainer();
  	private MenuCreator menuCreator;
  	private TextOperator textOperator;
  	private PredictiveParser parser;
  	private List<ActionListener> actionListeners = new ArrayList<ActionListener>();
  	private LoggerContext loggerContext;
  	private final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(this.getClass());
  	
  	private MenuBlockManager menuBlockManager;
  	private MenuBlock enlargedMenuBlock;
  	
  	private DirectHtml textArea = new DirectHtml();
  	private TabSensitiveTextField textField;
  	private TextField dummyTextField;
  	private Column menuBlockArea;
  	private GeneralButton deleteButton, clearButton, okButton, cancelButton;
  	
  	private String textAreaStartText = "";
  	private String textAreaEndText = "<span style=\"color: rgb(150, 150, 150)\"> ...</span>";
  	
  	// TODO: reactive key combinations
  //	private KeyStrokeListener keyStrokeListener = new KeyStrokeListener();
  
  	private boolean isInitialized = false;
  	
  	/**
  	 * Creates a new predictive editor window using the given predictive parser.
  	 * 
  	 * @param title The title of the window.
  	 * @param parser The predictive parser to be used. Do not modify this object while the
  	 *     preditor window is active!
  	 */
  	public PreditorWindow(String title, PredictiveParser parser) {
  		this.parser = parser;
  		this.menuBlockManager = new MenuBlockManager(this);
  		
  		addWindowPaneListener(this);
  		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
  		setModal(true);
  		setTitle(title);
  		setTitleFont(new Font(Style.fontTypeface, Font.ITALIC, new Extent(13)));
  		setWidth(new Extent(753));
  		setHeight(new Extent(524));
  		setResizable(false);
  		setTitleBackground(Style.windowTitleBackground);
  		setStyleName("Default");
  		
  		Grid grid = new Grid(1);
  		grid.setColumnWidth(0, new Extent(730));
  		add(grid);
  		
  		GridLayoutData layout = new GridLayoutData();
  		layout.setAlignment(new Alignment(Alignment.LEFT, Alignment.TOP));
  		
  		Column textAreaColumn = new Column();
  		textAreaColumn.setInsets(new Insets(10, 10, 10, 0));
  		textAreaColumn.add(textArea);
  		textAreaColumn.setLayoutData(layout);
  		grid.setRowHeight(0, new Extent(68));
  		grid.add(textAreaColumn);
  		
  		Column textColumn = new Column();
  		textColumn.setInsets(new Insets(10, 10, 0, 0));
  		textColumn.setCellSpacing(new Extent(10));
  		
  		Row textAreaButtonBar = new Row();
  		textAreaButtonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.CENTER));
  		textAreaButtonBar.setInsets(new Insets(0, 5, 10, 0));
  		textAreaButtonBar.setCellSpacing(new Extent(5));
  		clearButton = new GeneralButton(getLocalized("preditor_button_clear"), this, 100);
  		clearButton.setVisible(false);
  		textAreaButtonBar.add(clearButton);
  		deleteButton = new GeneralButton(getLocalized("preditor_button_delete"), this, 100);
  		textAreaButtonBar.add(deleteButton);
  		textColumn.add(textAreaButtonBar);
  		
  		Column textFieldColumn = new Column();
  		textFieldColumn.setCellSpacing(new Extent(1));
  		Label textFieldLabel = new Label(getLocalized("preditor_textfield_label"), Font.ITALIC, 11);
  		textFieldColumn.add(textFieldLabel);
  		
  		textField = new TabSensitiveTextField(this);
  		textField.setWidth(new Extent(708));
  		textField.setDisabledBackground(Style.lightDisabled);
  		Row textFieldRow = new Row();
  		textFieldRow.add(textField);
  		dummyTextField = new TextField();
  		dummyTextField.setWidth(new Extent(1));
  		dummyTextField.setBorder(new Border(0, Color.BLACK, 0));
  		dummyTextField.setBackground(Color.WHITE);
  		textFieldRow.add(dummyTextField);
  		textFieldColumn.add(textFieldRow);
  		
  //		keyStrokeListener.addKeyCombination(VK_TAB, "Tab");
  //		keyStrokeListener.addKeyCombination(VK_ESCAPE, "Esc");
  //		keyStrokeListener.addKeyCombination(VK_BACK_SPACE | CONTROL_MASK, "Ctrl-Backspace");
  //		keyStrokeListener.addActionListener(this);
  //		textFieldColumn.add(keyStrokeListener);
  		
  		textColumn.add(textFieldColumn);
  		grid.setRowHeight(1, new Extent(88));
  		grid.add(textColumn);
  		
  		menuBlockArea = new Column();
  		menuBlockArea.setInsets(new Insets(10, 15, 0, 0));
  		grid.setRowHeight(2, new Extent(275));
  		grid.add(menuBlockArea);
  		
  		Row buttonBar = new Row();
  		buttonBar.setAlignment(new Alignment(Alignment.RIGHT, Alignment.TOP));
  		buttonBar.setInsets(new Insets(10, 10, 10, 0));
  		buttonBar.setCellSpacing(new Extent(5));
  		okButton = new GeneralButton(getLocalized("general_action_ok"), this, 100);
  		buttonBar.add(okButton);
  		cancelButton = new GeneralButton(getLocalized("general_action_cancel"), this, 100);
  		buttonBar.add(cancelButton);
  		grid.setRowHeight(3, new Extent(30));
  		grid.add(buttonBar);
  		
  		update();
  	}
  	
  	/**
  	 * Sets the menu creator. {@link DefaultMenuCreator} is used by default.
  	 * 
  	 * @param menuCreator The menu creator.
  	 */
  	public void setMenuCreator(MenuCreator menuCreator) {
  		this.menuCreator = menuCreator;
  		update();
  	}
  	
  	/**
  	 * Returns the menu creator.
  	 * 
  	 * @return The menu creator.
  	 */
  	public MenuCreator getMenuCreator() {
  		if (menuCreator == null) {
  			setMenuCreator(new DefaultMenuCreator());
  		}
  		return menuCreator;
  	}
  	
  	/**
  	 * Sets the text operator. {@link DefaultTextOperator} is used by default.
  	 * 
  	 * @param textOperator The text operator.
  	 */
  	public void setTextOperator(TextOperator textOperator) {
  		this.textOperator = textOperator;
  		textContainer.setTextOperator(textOperator);
  	}
  	
  	/**
  	 * Returns the text operator.
  	 * 
  	 * @return The text operator.
  	 */
  	public TextOperator getTextOperator() {
  		if (textOperator == null) {
  			setTextOperator(new DefaultTextOperator());
  		}
  		return textOperator;
  	}
  	
  	/**
  	 * Shows or hides the "clear" button.
  	 * 
  	 * @param visible true to show the "clear" button; false to hide it.
  	 */
  	public void setClearButtonVisible(boolean visible) {
  		clearButton.setVisible(visible);
  	}
  	
  	/**
  	 * Sets the text to be shown in the text area in front of the text entered by the user. The
  	 * default is an empty string.
  	 * 
  	 * @param textAreaStartText The text, possibly enriched with HTML tags.
  	 */
  	public void setTextAreaStartText(String textAreaStartText) {
  		this.textAreaStartText = textAreaStartText;
  	}
  	
  	/**
  	 * Sets the text to be shown in the text area at the end of the text entered by the user. The
  	 * default are three gray dots "...".
  	 * 
  	 * @param textAreaEndText The text, possibly enriched with HTML tags.
  	 */
  	public void setTextAreaEndText(String textAreaEndText) {
  		this.textAreaEndText = textAreaEndText;
  	}
  	
  	/**
  	 * Returns a copy of the text container object that contains the (partial) text that has been
  	 * entered.
  	 * 
  	 * @return A copy of the text container object.
  	 */
  	public TextContainer getTextContainer() {
  		return textContainer.clone();
  	}
  	
  	/**
  	 * Returns the number of tokens of the current (partial) text.
  	 * 
  	 * @return The number of tokens.
  	 */
  	public int getTokenCount() {
  		return parser.getTokenCount();
  	}
  	
  	/**
  	 * Returns whether the given token is a possible next token.
  	 * 
  	 * @param token The token.
  	 * @return true if it is a possible next token.
  	 */
  	public boolean isPossibleNextToken(String token) {
  		return parser.isPossibleNextToken(token);
  	}
  	
  	/**
  	 * Adds the text element to the end of the text.
  	 * 
  	 * @param te The text element to be added.
  	 */
  	public void addTextElement(TextElement te) {
  		textElementSelected(te);
  		textField.setText("");
  		update();
  	}
  	
  	/**
  	 * Reads the text and adds it to the end of the current text as far as possible.
  	 * 
  	 * @param text The text to be added.
  	 */
  	public void addText(String text) {
  		handleTextInput(text, true);
  		update();
  	}
  	
  	private void textElementSelected(TextElement te) {
  		textContainer.addElement(te);
  		parser.addToken(te.getOriginalText());
  		
  		log("words added: " + te);
  	}
  	
  	private void update() {
  		if (!isInitialized) return;
  		updateMenuBlockContents();
  		menuBlockArea.removeAll();
  		menuBlockManager.setFilter(textField.getText());
  		if (enlargedMenuBlock != null) {
  			// One enlarged menu block
  			MenuBlockContent mbc = enlargedMenuBlock.getContent();
  			int cs = menuCreator.getColorShift(mbc.getName());
  			enlargedMenuBlock = new MenuBlock(708, 240, cs, this);
  			enlargedMenuBlock.setContent(mbc);
  			enlargedMenuBlock.setEnlarged(true);
  			menuBlockArea.add(enlargedMenuBlock);
  		} else {
  			menuBlockArea.add(menuBlockManager.createGUI());
  		}
  		textField.setEnabled(menuBlockManager.getMenuBlockCount() > 0 || !textField.getText().equals(""));
  		EchoThread.getActiveApplication().setFocusedComponent(textField);
  		clearButton.setEnabled(getTokenCount() > 0);
  		deleteButton.setEnabled(getTokenCount() > 0);
  	}
  	
  	private void updateMenuBlockContents() {
  		int ref = parser.getReference();
  		String t = "";
  		TextElement prev = null;
  		for (int i = 0; i < getTokenCount() ; i++) {
  			TextElement te = textContainer.getTextElement(i);
  			String glue = "";
  			if (prev != null) {
  				glue = getTextOperator().getGlue(prev, te);
  			}
  			if (ref > -1 && (ref == i || i == getTokenCount()-1)) {
  				t += glue + "<u>" + te.getText() + "</u>";
  			} else {
  				t += glue + te.getText();
  			}
  			prev = te;
  		}
  		if (t.startsWith(" ")) t = t.substring(1);
  		textArea.setText(
  				"<div style=\"font-family: Verdana,Arial,Helvetica,Sans-Serif; font-size: 12px\">" +
  				textAreaStartText +
  				t +
  				textAreaEndText +
  				"</div>"
  			);
  		
  		menuBlockManager.clear();
  		
  		NextTokenOptions options = parser.getNextTokenOptions();
  		HashMap<String, MenuBlockContent> contentsMap = new HashMap<String, MenuBlockContent>();
  		for (MenuItem m : getMenuCreator().createSpecialMenuItems(options)) {
  			addMenuItem(m, contentsMap);
  		}
  		for (ConcreteOption o : options.getConcreteOptions()) {
  			addMenuItem(getMenuCreator().createMenuEntry(o), contentsMap);
  		}
  		
  		for (String mg : getMenuCreator().getMenuGroupOrdering()) {
  			if (contentsMap.containsKey(mg)) {
  				menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
  			}
  		}
  		for (String mg : contentsMap.keySet()) {
  			if (!getMenuCreator().getMenuGroupOrdering().contains(mg)) {
  				menuBlockManager.addMenuBlockContent(contentsMap.get(mg));
  			}
  		}
  	}
  	
  	private void addMenuItem(MenuItem menuItem, Map<String, MenuBlockContent> contentsMap) {
  		String menuGroup = menuItem.getMenuGroup();
  		MenuBlockContent mbc;
  		if (contentsMap.containsKey(menuGroup)) {
  			mbc = contentsMap.get(menuGroup);
  		} else {
  			mbc = new MenuBlockContent(menuGroup);
  			mbc.setComparator(getMenuCreator().getMenuItemComparator());
  			mbc.setActionListener(this);
  			contentsMap.put(menuGroup, mbc);
  		}
  		mbc.addItem(menuItem);
  	}
  	
  	private void handleTextInput(boolean enterPressed) {
  		handleTextInput(textField.getText(), enterPressed);
  	}
  	
  	private void handleTextInput(String text, boolean enterPressed) {
  		List<String> subtokens = getTextOperator().splitIntoTokens(text);
  		boolean force = enterPressed && (text.equals(menuBlockManager.getFilter()) || text.endsWith(" "));
  		handleTokenInput(subtokens, force, true);
  	}
  	
  	private void handleTokenInput(List<String> subtokens, boolean force, boolean caseSensitive) {
  		if (subtokens.size() == 0) {
  			textField.setText("");
  			return;
  		}
  		
  		String filter = "";
  		for (String s : subtokens) filter += s + " ";
  		menuBlockManager.setFilter(filter);
  		
  		String text = "";
  		TextElement textElement = null;
  		List<String> rest = null;
  		List<String> s = new ArrayList<String>(subtokens);
  		while (s.size() > 0) {
  			if (text.length() > 0) text += " ";
  			text += s.remove(0);
  			TextElement te = null;
  			if (caseSensitive) {
  				te = getTextOperator().createTextElement(text);
  			} else {
  				String t = proposeToken(text);
  				if (t != null) {
  					te = getTextOperator().createTextElement(t);
  				}
  			}
  			if (te != null && parser.isPossibleNextToken(te.getOriginalText())) {
  				textElement = te;
  				rest = new ArrayList<String>(s);
  			}
  		}
  		if (textElement != null) {
  			if ((rest.isEmpty() && force) || (!rest.isEmpty() &&
  					menuBlockManager.getMenuEntryCount() == 0)) {
  				textElementSelected(textElement);
  				updateMenuBlockContents();
  				handleTokenInput(rest, force, caseSensitive);
  				return;
  			}
  		}
  		if (caseSensitive) {
  			handleTokenInput(subtokens, force, false);
  		} else {
  			textField.setText(text);
  		}
  	}
  	
  	private String proposeToken(String text) {
  		text = text.toLowerCase().replaceAll("\\s+", "_");
  		for (ConcreteOption o : parser.getNextTokenOptions().getConcreteOptions()) {
  			String t = o.getWord().toLowerCase().replaceAll("\\s+", "_");
  			if (t.equals(text)) {
  				return o.getWord();
  			}
  		}
  		return null;
  	}
  	
  	/**
  	 * Adds a new action-listener.
  	 * 
  	 * @param actionListener The new action-listener.
  	 */
  	public void addActionListener(ActionListener actionListener) {
  		actionListeners.add(actionListener);
  	}
  	
  	/**
  	 * Removes the action-listener.
  	 * 
  	 * @param actionListener The action-listener to be removed.
  	 */
  	public void removeActionListener(ActionListener actionListener) {
  		actionListeners.remove(actionListener);
  	}
  	
  	/**
  	 * Removes all action-listeners.
  	 */
  	public void removeAllActionListeners() {
  		actionListeners.clear();
  	}
  	
  	private void notifyActionListeners(ActionEvent event) {
  		for (ActionListener al : actionListeners) {
  			al.actionPerformed(event);
  		}
  	}
  	
  	public void actionPerformed(ActionEvent e) {
  		Object src = e.getSource();
  		String c = e.getActionCommand();
  
  		if (enlargedMenuBlock != null) {
  			enlargedMenuBlock.setEnlarged(false);
  			enlargedMenuBlock = null;
  		}
  		
  		boolean tabKeyPressed = false;
  		
  		if (src == cancelButton) {
  			log("pressed: cancel");
  			notifyActionListeners(new ActionEvent(this, "Cancel"));
  			return;
  		} else if (src == okButton) {
  			log("pressed: ok");
  			handleTextInput(true);
  			update();
  			notifyActionListeners(new ActionEvent(this, "OK"));
  			return;
  		} else if (src == deleteButton) {
  			log("pressed: < delete");
  			removeLastToken();
  		} else if (src == clearButton) {
  			log("pressed: clear");
  			clearTokens();
  		} else if (src == textField) {
  			if (getApplicationInstance().getFocusedComponent() == dummyTextField) {
  				log("pressed: tab-key");
  				handleTextInput(false);
  				tabKeyPressed = true;
  			} else {
  				log("pressed: enter-key");
  				if (textField.getText().equals("") && menuBlockManager.getFilter().equals("")) {
  					notifyActionListeners(new ActionEvent(this, "Enter"));
  					return;
  				} else {
  					handleTextInput(true);
  				}
  			}
  		} else if (src instanceof MenuEntry) {
  			TextElement te = ((MenuEntry) e.getSource()).getTextElement();
  			log("pressed: menu-entry " + te.getText());
  			textElementSelected(te);
  			textField.setText("");
  		} else if ("enlarge".equals(c) && src instanceof MenuBlock) {
  			enlargedMenuBlock = (MenuBlock) src;
  		} else if ("Esc".equals(c)) {
  			log("pressed: escape key");
  			notifyActionListeners(new ActionEvent(this, "Escape"));
  			return;
  		} else if ("Ctrl-Backspace".equals(c)) {
  			log("pressed: ctrl-backspace");
  			if (getTokenCount() > 0) {
  				textContainer.removeLastElement();
  				parser.removeToken();
  				textField.setText("");
  			}
  		}
  		
  		update();
  		
  		if (tabKeyPressed) {
  			String s = menuBlockManager.getStartString();
  			if (s != null) textField.setText(s);
  		}
  	}
  	
  	/**
  	 * Removes the last token.
  	 */
  	public void removeLastToken() {
  		if (getTokenCount() > 0) {
  			textContainer.removeLastElement();
  			parser.removeToken();
  			textField.setText("");
  		}
  	}
  	
  	/**
  	 * Removes all tokens.
  	 */
  	public void clearTokens() {
  		textContainer.removeAllElements();
  		parser.removeAllTokens();
  		textField.setText("");
  	}
  	
  	/**
  	 * Returns true if the current text is a complete statement.
  	 * 
  	 * @return true if the current text is a complete statement.
  	 */
  	public boolean isTextComplete() {
  		return parser.isComplete();
  	}
  	
  	/**
  	 * Returns the predictive parser. Do not modify this object while the preditor window is
  	 * active!
  	 * 
  	 * @return The predictive parser.
  	 */
  	public PredictiveParser getPredictiveParser() {
  		return parser;
  	}
  	
  	/**
  	 * Returns a localized string.
  	 * 
  	 * @param key The text key.
  	 * @return The text.
  	 */
  	protected String getLocalized(String key) {
  		return LocaleResources.getString(key);
  	}
  	
  	public void windowPaneClosing(WindowPaneEvent e) {
  		log("pressed: close window");
  		notifyActionListeners(new ActionEvent(this, "Close"));
  	}
  	
  	public void init() {
  		isInitialized = true;
  		update();
  		super.init();
  	}
  
  	/**
  	 * Sets the logger context.
  	 * 
  	 * @param loggerContext The logger context object or null.
  	 */
  	public void setLoggerContext(LoggerContext loggerContext) {
  		this.loggerContext = loggerContext;
  	}
  	
  	private void log(String text) {
  		if (loggerContext != null) {
  			loggerContext.propagateWithinThread();
  			org.slf4j.MDC.put("type", "pred");
  			log.info(text);
  		}
  	}
  	
  	public String toString() {
  		return "sentence: " + textContainer.getText();
  	}
  	
  }
  