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.List;
  
  import nextapp.echo.app.Component;
  import nextapp.echo.app.Extent;
  import nextapp.echo.app.Row;
  
  /**
   * This is a helper class that takes care of the management and layout of the menu blocks for the
   * predictive editor.
   * 
   * @author Tobias Kuhn
   */
  class MenuBlockManager {
  
  	private List<MenuBlockContent> mbContents = new ArrayList<MenuBlockContent>();
  	private List<MenuBlockColumn> mbColumns = new ArrayList<MenuBlockColumn>();
  	private PreditorWindow preditor;
  	private String filter;
  	
  	/**
  	 * Creates a new menu block manager.
  	 * 
  	 * @param preditor The predictive editor object.
  	 */
  	public MenuBlockManager(PreditorWindow preditor) {
  		this.preditor = preditor;
  	}
  	
  	/**
  	 * Returns the number of menu blocks.
  	 * 
  	 * @return The number of menu blocks.
  	 */
  	public int getMenuBlockCount() {
  		return mbContents.size();
  	}
  	
  	/**
  	 * Adds a menu block with the given content.
  	 * 
  	 * @param m The content of the menu block to be added.
  	 */
  	public void addMenuBlockContent(MenuBlockContent m) {
  		if (!m.isEmpty()) {
  			mbContents.add(m);
  		}
  	}
  	
  	/**
  	 * Removes all menu blocks.
  	 */
  	public void clear() {
  		mbContents.clear();
  	}
  	
  	/**
  	 * Returns the number of menu entries.
  	 * 
  	 * @return The number of menu entries.
  	 */
  	public int getMenuEntryCount() {
  		int c = 0;
  		for (MenuBlockContent mc : mbContents) {
  			c += mc.getEntryCount();
  		}
  		return c;
  	}
  	
  	/**
  	 * Sets the filter for the menu entries.
  	 * 
  	 * @param filter The filter to be set.
  	 */
  	public void setFilter(String filter) {
  		if (filter == null) filter = "";
  		filter = filter.replaceFirst("^\\s*", "").replaceFirst("\\s*$", "");
  		
  		for (MenuBlockContent c : mbContents) {
  			c.setFilter(filter);
  		}
  		this.filter = filter;
  	}
  	
  	/**
  	 * Returns the current filter for the menu entries.
  	 * 
  	 * @return The filter.
  	 */
  	public String getFilter() {
  		return filter;
  	}
  	
  	/**
  	 * Returns the longest possible string with which all menu entries start (after filtering). An
  	 * empty string is returned if at least two entries start with a different character.
  	 * 
  	 * @return The start string.
  	 */
  	public String getStartString() {
  		String startString = "";
  		List<String> blockStartStrings = new ArrayList<String>();
  		
  		for (MenuBlockContent mc : mbContents) {
  			String s = mc.getStartString();
  			if (s != null) {
  				blockStartStrings.add(s);
  			}
  		}
  		
  		if (blockStartStrings.isEmpty()) return null;
  		
  		String first = blockStartStrings.get(0);
  		blockStartStrings.remove(0);
  		
  		if (blockStartStrings.isEmpty()) return first;
  		
  		for (int i = 0; i < first.length(); i++) {
  			char c = first.charAt(i);
  			boolean stop = false;
  			for (String s : blockStartStrings) {
  				if (s.length() <= i || s.charAt(i) != c) stop = true;
  			}
  			if (stop) break;
  			startString += c;
  		}
  		
  		return startString;
  	}
  	
  	/**
  	 * Creates an returns the GUI component showing all menu blocks.
  	 * 
  	 * @return The GUI component.
  	 */
  	public synchronized Component createGUI() {
  		mbColumns.clear();
  		for (MenuBlockContent c : mbContents) {
  			mbColumns.add(new MenuBlockColumn(c, preditor));
  		}
  		condenseColumns();
  		Row menuBlockRow = new Row();
  		menuBlockRow.setCellSpacing(new Extent(9));
  		int colCount = mbColumns.size();
  		int width = ( 720 / ( colCount > 3 ? colCount : 3 ) ) - 12;
  		for (MenuBlockColumn c : mbColumns) {
  			menuBlockRow.add(c.createGUI(width));
  		}
  		return menuBlockRow;
  	}
  	
  	/**
  	 * This method condenses the columns by putting several menu blocks into the same column.
  	 */
  	private void condenseColumns() {
  		int i = mbColumns.size()-1;
  		while (i > 0 && mbColumns.size() > 3) {
  			MergeStep step = new MergeStep(i-1);
  			if (step.getHeightUnits() <= 18) {
  				step.apply();
  			}
  			i--;
  		}
  		while (hasTooManyColumns()) {
  			CondenseStep step = new MergeStep(0);
  			for (int j = 1; j < mbColumns.size()-1; j++) {
  				CondenseStep s = new MergeStep(j);
  				if (s.getWeightDiff() < step.getWeightDiff()) {
  					step = s;
  				}
  				s = new SplitStep(j);
  				if (s.getWeightDiff() < step.getWeightDiff()) {
  					step = s;
  				}
  			}
  			step.apply();
  		}
  	}
  	
  	/**
  	 * This method returns whether the number of columns has to be condensed further or not.
  	 * 
  	 * @return Whether there are still too many columns.
  	 */
  	private boolean hasTooManyColumns() {
  		return mbContents.size() < mbColumns.size()*(mbColumns.size()-2);
  	}
  	
  	
  	/**
  	 * This internal interface represents a step of condensing the columns.
  	 */
  	private interface CondenseStep {
  		
  		public int getWeightDiff();
  		
  		public void apply();
  		
  	}
  	
  	/**
  	 * This internal class represents a condensing step of merging two columns.
  	 */
  	private class MergeStep implements CondenseStep {
  		
  		private MenuBlockColumn c1, c2;
  		private int diff;
  		private int heightUnits;
  		
  		MergeStep(int i) {
  			c1 = mbColumns.get(i);
  			c2 = mbColumns.get(i+1);
  			diff = MenuBlockColumn.getCombinedWeight(c1, c2) - c1.getWeight() - c2.getWeight();
  			heightUnits = c1.getHeightUnits() + c2.getHeightUnits();
  		}
  		
  		public int getWeightDiff() {
  			return diff;
  		}
  		
  		public int getHeightUnits() {
  			return heightUnits;
  		}
  		
  		public void apply() {
  			c1.includeAtBottom(c2);
  			mbColumns.remove(c2);
  		}
  		
  	}
  	
  	/**
  	 * This internal class represents a condensing step of splitting one column by moving its menu
  	 * blocks to the adjacent columns.
  	 */
  	private class SplitStep implements CondenseStep {
  
  		private MenuBlockColumn c1, s, c2;
  		private MenuBlockColumn[] splitted;
  		private int diff;
  		
  		SplitStep(int i) {
  			c1 = mbColumns.get(i-1);
  			s = mbColumns.get(i);
  			c2 = mbColumns.get(i+1);
  			splitted = s.getSplitted(c2.getWeight() - c1.getWeight());
  			int w1 = MenuBlockColumn.getCombinedWeight(c1, splitted[0]);
  			int w2 = MenuBlockColumn.getCombinedWeight(c2, splitted[1]);
  			diff = w1 + w2 - c1.getWeight() - s.getWeight() - c2.getWeight();
  		}
  		
  		public int getWeightDiff() {
  			return diff;
  		}
  		
  		public void apply() {
  			c1.includeAtBottom(splitted[0]);
  			c2.includeAtTop(splitted[1]);
  			mbColumns.remove(s);
  		}
  		
  	}
  
  }
  