/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.struts2.views.java.simple;

import nuts.core.bean.Beans;
import nuts.core.lang.Collections;
import nuts.core.lang.Strings;
import nuts.exts.struts2.components.Form;
import nuts.exts.struts2.components.ListView;
import nuts.exts.struts2.components.Pager;
import nuts.exts.struts2.components.Property;
import nuts.exts.struts2.components.URL;
import nuts.exts.struts2.util.StrutsContextUtils;
import nuts.exts.struts2.views.java.AbstractTemplateRenderer;
import nuts.exts.struts2.views.java.Attributes;
import nuts.exts.xwork2.PermissionValidator;
import nuts.exts.xwork2.util.ContextUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.struts2.components.template.TemplateRenderingContext;
import org.apache.struts2.util.MakeIterator;

public class NutsListViewRenderer extends AbstractTemplateRenderer {

	private String ids;
	private String id;

	private String start;
	private String limit;
	private String total;
	private String sort;
	private String dir;
	private String qfilters;
	private String qfilterm;
	
	private boolean script;
	private boolean uiTheme;
	
	private Object columns;
	private String[] cssColumns;
	private String listn;
	private int listz;
	
	private boolean sortable;
	private Map<String, List<String>> fieldErrors;
	
	private String _sort;
	private String _dir;
	private Integer _start;
	private Map _link;
	private Set<String> _fsdl = new HashSet<String>();
	private Set<String> _fsvl = new HashSet<String>();
	
	private PermissionValidator _permit;
	
	private Map<String, Map> codemaps = new HashMap<String, Map>();

	public NutsListViewRenderer(TemplateRenderingContext context) {
		super(context);
	}

	@SuppressWarnings("unchecked")
	private void initVars() {
		_permit = ContextUtils.findPermissionValidatorInStack(stack);

		id = (String)params.get("id");
		ids = html(id);

		sort = (String)params.get("sort");
		if (Strings.isNotEmpty(sort)) {
			_sort = findString(sort);
		}
		
		dir = (String)params.get("dir");
		if (Strings.isNotEmpty(dir)) {
			_dir = defs(findString(dir)).toLowerCase();
		}
		
		start = (String)params.get("start");
		limit = (String)params.get("limit");
		total = (String)params.get("total");
		qfilters = (String)params.get("filters");
		qfilterm = (String)params.get("filterm");

		listn = (String)params.get("listName");
		listz = (Integer)params.get("listSize");

		columns = params.get("columns");
		cssColumns = Strings.split(defs(params.get("cssColumn")));
		
		sortable = Attributes.isTrue(params.get("sortable"), true);
		
		_start = (Integer)findValue(start, Integer.class);
		if (_start == null) {
			_start = 0;
		}
		
		_link = (Map)params.get("link");

		script = Attributes.isTrue(params.get("script"), true);
		
		fieldErrors = (Map<String, List<String>>)findValue("fieldErrors");
		
		uiTheme = Attributes.isTrue(params.get("uiTheme"), true);
	}

	public void render() throws IOException {
		initVars();
		
		Attributes a = new Attributes();
		
		String cssClass = (String)params.get("cssClass");
		a.add("id", id)
		 .add("class", "n-lv" + (cssClass == null ? "" : " " + cssClass))
		 .addIfExists("singleSelect", params.get("singleSelect"))
		 .addIfExists("toggleSelect", params.get("toggleSelect"))
		 .addIfExists("onrowclick", params.get("onrowclick"))
		 .cssStyle(params);
		stag("div", a);
		
		writeListViewForm();

		writeListViewTools();
		
		writeListViewHeader();
		
		writeListViewTable();

		writeListViewFooter();
		
		etag("div");
	}

	protected String uiClass(String cls) {
		return uiTheme ? ' ' + cls : "";
	}
	
	protected void writeJsc(String js) throws IOException {
		if (script) {
			super.writeJsc(js);
		}
	}

	private void writeListViewHiddens() throws IOException {
		final String[] hns = new String[] { 
				"start", "limit", "total", "sort", "dir", "filterm" }; 
		for (String hn : hns) {
			String n = (String)params.get(hn);
			if (Strings.isNotEmpty(n)) {
				writeHidden(id + '_' + hn, n);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void writeListViewForm() throws IOException {
		Form form = new Form(stack,
				StrutsContextUtils.getServletRequest(),
				StrutsContextUtils.getServletResponse());

		StrutsContextUtils.getContainer(stack).inject(form);

		form.setId(id + "_form");
		form.setCssClass("n-lv-form");
		form.setAction((String)params.get("action"));
		form.setMethod(defs((String)params.get("method"), "get"));
		form.setOnsubmit((String)params.get("onsubmit"));
		form.setOnreset((String)params.get("onreset"));
		form.setLoadmask("false");

		form.start(writer);

		writeListViewHiddens();
		
		String _fs = qfilters;
		if (Strings.isNotEmpty(_fs)) {
			Map<String, Object> _fsm = (Map)findValue(_fs);
			if (Collections.isNotEmpty(_fsm)) {
				for (Entry<String, Object> en : _fsm.entrySet()) {
					writeHidden(id + "_fs_" + en.getKey() + "_c",
							_fs + "." + en.getKey() + ".c");
					String t = defs(findString(_fs + "." + en.getKey() + ".t"), "s");
					Object vs = findValue(_fs + "." + en.getKey() + ".vs");
					if (vs != null) {
						int i = 0;
						String d = id + "_fs_" + en.getKey() + "_v_" + i;
						String n = _fs + "." + en.getKey() + "." + t + "vs";
						Iterator it = MakeIterator.convert(vs);
						while (it.hasNext()) {
							writeHidden(d, n, formatRawValue(it.next()));
						}
					}
				}
			}
		}
		
//		String _pq = (String)params.get("query");
//		if (StringUtils.isNotEmpty(_pq)) {
//			writeHidden(id + "_query_name", _pq + ".n");
//			writeHidden(id + "_query_kind", _pq + ".k");
//			
//			Map<String, Object> _pqfsv = (Map)findValue(_pq + ".fs");
//			if (CollectionUtils.isNotEmpty(_pqfsv)) {
//				for (Entry<String, Object> en : _pqfsv.entrySet()) {
//					writeHidden(id + "_query_fs_" + en.getKey() + "_c",
//							_pq + ".fs." + en.getKey() + ".c");
//					String t = defs(findString(_pq + ".fs." + en.getKey() + ".t"), "s");
//					Object vs = findValue(_pq + ".fs." + en.getKey() + ".vs");
//					if (vs != null) {
//						int i = 0;
//						String d = id + "_query_fs_" + en.getKey() + "_v_" + i;
//						String n = _pq + ".fs." + en.getKey() + "." + t + "vs";
//						Iterator it = MakeIterator.convert(vs);
//						while (it.hasNext()) {
//							writeHidden(d, n, formatRawValue(it.next()));
//						}
//					}
//				}
//			}
//		}
		
		Attributes a = new Attributes();
		a.add("type", "submit")
		 .add("class", "n-lv-submit")
		 .add("id", id + "_submit");
		xtag("input", a);
		
		form.end(writer, "");
	}
	
	private Object getBeanProperty(Object bean, String name) {
		try {
			return Beans.getBean(bean, name);
		}
		catch (Exception e) {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	private <T> T getBeanProperty(Object bean, String name, T defv) {
		T v = null;
		try {
			v = (T)Beans.getBean(bean, name);
		}
		catch (Exception e) {
		}
		return v == null ? defv : v;
	}

	private void writePager(String pos) throws IOException {
		Pager pager = new Pager(stack,
				StrutsContextUtils.getServletRequest(),
				StrutsContextUtils.getServletResponse());

		StrutsContextUtils.getContainer(stack).inject(pager);
		
		pager.setId(id + "_pager_" + pos);
		pager.setCssClass("n-lv-pager");
		pager.setStart(start);
		pager.setCount(listz);
		pager.setLimit(limit);
		pager.setTotal(total);
		pager.setOnLinkClick("_nlv_goto('" + id + "', #)");
		pager.setOnLimitChange("_nlv_limit('" + id + "', this.value)");
		
		pager.start(writer);
		pager.end(writer, "");
	}
	
	private void writeListViewTools() throws IOException {
		write("<div id=\"");
		write(ids);
		write("_tools\" class=\"n-lv-tools\">");
		
		writeListViewFilters();
		
		write("</div>");
	}
	
	private void writeListViewHeader() throws IOException {
		boolean pager = Attributes.isTrue(params.get("headPager"), false);
		String tools = (String)params.get("headTools");
		String addon = (String)params.get("headAddon");

		if (!pager && Strings.isEmpty(tools) && Strings.isEmpty(addon)) {
			return;
		}

		write("<div id=\"");
		write(ids);
		write("_header\" class=\"n-lv-header\">");
		
		write("<div class=\"n-lv-toolbar" + uiClass("n-table-toolbar") + "\">");
		write("<table class=\"n-lv-toolbar-tb\">");
		write("<tr>");
		
		if (pager) {
			write("<td class=\"n-lv-td-pager\">");
			writePager("header");
			write("</td>");
		}

		if (Strings.isNotEmpty(tools)) {
			write("<td class=\"n-lv-td-tools\">");
			write("<div class=\"n-lv-tools\">");
			writeCheckAllButton();
			write(tools);
			write("</div>");
			write("</td>");
		}

		if (Strings.isNotEmpty(addon)) {
			write("<td class=\"n-lv-td-addon\">");
			write("<div class=\"n-lv-addon\">");
			write(addon);
			write("</div>");
			write("</td>");
		}

		write("</tr>");
		write("</table>");
		write("</div>");

		write("</div>");
	}
	
	private void writeCheckAllButton() throws IOException {
		if (Attributes.isTrue(params.get("checkAllToolButton"), true)) {
			Iterator itc = MakeIterator.convert(columns);
			while (itc.hasNext()) {
				Object c = itc.next();
				String ctype = (String)getBeanProperty(c, "type");
				if ("check".equals(ctype)) {
					Map<String, String> as = new HashMap<String, String>();
					as.put("cssClass", "n-lv-cab");
					as.put("textSelectAll", getText("button-selectAll"));
					as.put("textSelectNone", getText("button-selectNone"));
					as.put("iconSelectAll", getText("icon-selectAll"));
					as.put("iconSelectNone", getText("icon-selectNone"));
					as.put("onclick", "return _nlv_onAllClick(this);");
					
					write(button(as.get("textSelectAll"), ticon(as.get("iconSelectAll")), null, as));

					break;
				}
			}
		}
	}
	
	private void writeListViewFooter() throws IOException {
		boolean pager = Attributes.isTrue(params.get("footPager"), false);
		String tools = (String)params.get("footTools");
		String addon = (String)params.get("footAddon");

		if (!pager && Strings.isEmpty(tools) && Strings.isEmpty(addon)) {
			return;
		}

		write("<div id=\"");
		write(ids);
		write("_footer\" class=\"n-lv-footer\">");
		
		write("<div class=\"n-lv-toolbar" + uiClass("ui-widget-header") + "\">");
		write("<table class=\"n-lv-toolbar-tb\">");
		write("<tr>");
		
		if (pager) {
			write("<td class=\"n-lv-td-pager\">");
			writePager("footer");
			write("</td>");
		}

		if (Strings.isNotEmpty(tools)) {
			write("<td class=\"n-lv-td-tools\">");
			write("<div class=\"n-lv-tools\">");
			writeCheckAllButton();
			write(tools);
			write("</div>");
			write("</td>");
		}

		if (Strings.isNotEmpty(addon)) {
			write("<td class=\"n-lv-td-addon\">");
			write("<div class=\"n-lv-addon\">");
			write(addon);
			write("</div>");
			write("</td>");
		}

		write("</tr>");
		write("</table>");
		write("</div>");

		write("</div>");
	}

	@SuppressWarnings("unchecked")
	private void writeListViewFiltersItems(Map<String, Filter> fm) throws IOException {
		ListView tag = (ListView)context.getTag();

		Map<String, String> stringFilterMap = tag.getStringFilterMap();
		Map<String, String> boolFilterMap = tag.getBoolFilterMap(); 
		Map<String, String> numberFilterMap = tag.getNumberFilterMap();
		Map<String, String> dateFilterMap = tag.getDateFilterMap();

		String _fs = (String)params.get("filters");

		for (Entry<String, Filter> en : fm.entrySet()) {
			Filter _f = en.getValue();
			String _name = en.getKey();
			String _fn = _fs + '.' + _name;
			String _ifn = id + "_fsf_" + _name;
			boolean _fd = _fsvl.contains(_name);

			write("<tr class=\"n-tr-input n-lv-fsi-" 
					+ html(_name) 
					+ (_fd ? "" : " n-hidden")
					+ "\">");

			boolean _hfe = false;
			if (fieldErrors != null) {
				for (Entry<String, List<String>> en2 : fieldErrors.entrySet()) {
					if (en2.getKey().startsWith(_fn + '.')) {
						_hfe = true;
						break;
					}
				}
			}

			write("<td class=\"n-td-label\">");
			write("<label for=\"" + _ifn + "_v\" class=\"" + (_hfe ? "n-label-error" : "n-label") + "\">");
			if (Strings.isNotEmpty(_f.label)) {
				write(html(_f.label));
				write(":");
			}
			write("</label></td>");

			write("<td class=\"n-td-input\"");
			if (Strings.isNotEmpty(_f.tooltip)) {
				write("title=\"");
				write(html(_f.tooltip));
				write("\"");
			}
			write(">");

			String _fv, _fvv;
			if ("string".equals(_f.type)) {
				_fv = _fn + ".sv";
				_fvv = "%{" + _fn + ".sv}";
				writeTextField("n-lv-f-string-v",
						_fv, _ifn + "_v", _fvv);
				
				_fvv = "%{" + _fn + ".c}";
				
				writeSelect("n-lv-f-string-c",
						_fn + ".c", _ifn + "_c", 
						stringFilterMap, _fvv, 
						null, null);
			}
			else if ("boolean".equals(_f.type)) {
				_fv = _fn + ".bv";
				write("<input type=\"hidden\" name=\"" + _fn + ".c\" value=\"in\"/>");
				write("<div class=\"n-checkboxlist\">");

				write("<span class=\"n-checkbox-item\">");
				Attributes ia = new Attributes();
				ia.add("type", "checkbox")
				  .add("class", "checkbox n-lv-f-boolean-true")
				  .add("name", _fv)
				  .add("id", _ifn + "_v")
				  .add("value", "true")
				  .addIfTrue("checked", findValue(_fv));
				xtag("input", ia);
				write("<label class=\"n-checkbox-label\" for=\"" + _ifn + "_v\">");
				write(boolFilterMap.get("true"));
				write("</label></span>");
				
				_fv = _fn + ".bv2";
				write("<span class=\"n-checkbox-item\">");
				ia = new Attributes();
				ia.add("type", "checkbox")
				  .add("class", "checkbox n-lv-f-boolean-false")
				  .add("name", _fv)
				  .add("id", _ifn + "_v2")
				  .add("value", "false")
				  .addIfTrue("checked", "false".equals(findValue(_fv)));
				xtag("input", ia);
				write("<label class=\"n-checkbox-label\" for=\"" + _ifn + "_v2\">");
				write(boolFilterMap.get("false"));
				write("</label></span>");
				
				write("</div>");
			}
			else if ("number".equals(_f.type)) {
				_fv = _fn + ".nv";
				_fvv = "%{" + _fn + ".nv}";

				writeTextField("n-lv-f-number-v",
						_fv, _ifn + "_v", _fvv);
				
				_fvv = "%{" + _fn + ".c}";
				writeSelect("n-lv-f-number-c",
						_fn + ".c", _ifn + "_c", 
						numberFilterMap, _fvv, 
						null, "_nlv_onBetweenChange(this)");
				
				_fv = _fn + ".nv2";
				_fvv = "%{" + _fn + ".nv2}";
				writeTextField("n-lv-f-number-v2",
						_fv, _ifn + "_v2", _fvv);

				writeJsc("$(function() { $('#" + jsstr(_ifn) + "_c').trigger('change'); });");
			}
			else if ("date".equals(_f.type)) {
				_fv = _fn + ".dv";
				_fvv = "%{" + _fn + ".dv}";

				String _ifns = jsstr(_ifn);
				String ops = "{ beforeShow: function(i) {"
					+ " return {"
					+ " minDate: (i.id == '" + _ifns + "_v2' ? $('#" + _ifns + "_v').datepicker('getDate') : null),"
					+ " maxDate: (i.id == '" + _ifns + "_v' ? $('#" + _ifns + "_v2').datepicker('getDate') : null)"
					+ "};"
					+ " } }";
				writeDatePicker("n-lv-f-date-v",
						_fv, _ifn + "_v", "date", _fvv, ops);

				_fvv = "%{" + _fn + ".c}";
				writeSelect("select n-lv-f-date-c",
						_fn + ".c", _ifn + "_c", 
						dateFilterMap, _fvv, 
						null, "_nlv_onBetweenChange(this)");
				
				_fv = _fn + ".dv2";
				_fvv = "%{" + _fn + ".dv2}";
				writeDatePicker("n-lv-f-date-v2",
						_fv, _ifn + "_v2", "date", _fvv, ops);

				writeJsc("$(function() { $('#" + _ifns + "_c').trigger('change'); });");
			}
			else if ("datetime".equals(_f.type)) {
				_fv = _fn + ".dv";
				_fvv = "%{" + _fn + ".dv}";

				String _ifns = jsstr(_ifn);
				writeDateTimePicker("n-lv-f-datetime-v",
						_fv, _ifn + "_v", "datetime", _fvv);

				_fvv = "%{" + _fn + ".c}";
				writeSelect("n-lv-f-datetime-c",
						_fn + ".c", _ifn + "_c", 
						dateFilterMap, _fvv, 
						null, "_nlv_onBetweenChange(this)");
				
				_fv = _fn + ".dv2";
				_fvv = "%{" + _fn + ".dv2}";
				writeDateTimePicker("n-lv-f-datetime-v2",
						_fv, _ifn + "_v2", "datetime", _fvv);

				writeJsc("$(function() { $('#" + _ifns + "_c').trigger('change'); });");
			}
			else if ("time".equals(_f.type)) {
				_fv = _fn + ".dv";
				_fvv = "%{" + _fn + ".dv}";

				String _ifns = jsstr(_ifn);
				writeTimePicker("n-lv-f-time-v",
						_fv, _ifn + "_v", "time", _fvv);

				_fvv = "%{" + _fn + ".c}";
				writeSelect("n-lv-f-time-c",
						_fn + ".c", _ifn + "_c", 
						dateFilterMap, _fvv, 
						null, "_nlv_onBetweenChange(this)");
				
				_fv = _fn + ".dv2";
				_fvv = "%{" + _fn + ".dv2}";
				writeTimePicker("n-lv-f-time-v2",
						_fv, _ifn + "_v2", "time", _fvv);

				writeJsc("$(function() { $('#" + _ifns + "_c').trigger('change'); });");
			}
			else if ("checklist".equals(_f.type)) {
				write("<input type=\"hidden\" name=\"" + _fn + ".c\" value=\"in\"/>");
				
				_fv = _fn + ".svs";
				_fvv = "%{" + _fn + ".svs}";
				writeCheckboxList("n-lv-f-checklist", 
						_fv, _ifn + "_v", _f.list, _fvv);
			}
			else if ("radio".equals(_f.type)) {
				write("<input type=\"hidden\" name=\"" + _fn + ".c\" value=\"eq\"/>");

				_fv = _fn + ".sv";
				_fvv = "%{" + _fn + ".sv}";
				writeRadio("n-lv-f-checklist", 
						_fv, _ifn + "_v", _f.list, _fvv);
			}
			else if ("select".equals(_f.type)) {
				write("<input type=\"hidden\" name=\"" + _fn + ".c\" value=\"eq\"/>");
				
				_fv = _fn + ".sv";
				_fvv = "%{" + _fn + ".sv}";
				writeSelect("n-lv-f-select",
						_fv, _ifn + "_v", 
						_f.list, _fvv, 
						"true", null);
			}
			
			if (Collections.isNotEmpty(fieldErrors)) {
				for (Entry<String, List<String>> fen : fieldErrors.entrySet()) {
					if (fen.getKey().startsWith(_fn + ".")) {
						write("<ul errorFor=\"" + html(_ifn) + "\" class=\"n-field-errors\">");
						for (String m : fen.getValue()) {
							write("<li class=\"n-field-error\">");
							write(icon("n-icon n-icon-error n-field-error"));
							write(html(m));
							write("</li>");
						}
						write("</ul>");
					}
				}
			}
			write("</td>");
			write("<td class=\"n-td-tooltip\">");
			if (!_f.fixed) {
				write(icon("n-icon n-icon-textfield_delete", "_nlv_onDelFilter(this, '" + html(_name) + "')"));
			}
			write("</td>");
			write("</tr>");
		}
	}

	@SuppressWarnings("unchecked")
	private void writeListViewFiltersOptions(Map<String, Filter> fm) throws IOException {
		// get editable filters
		Map<String, Filter> fm2 = new LinkedHashMap<String, Filter>();
		for (Entry<String, Filter> en : fm.entrySet()) {
			String _name = en.getKey();
			Filter _f = en.getValue();
			if (!_f.fixed) {
				fm2.put(_name, _f);
			}
		}

		ListView tag = (ListView)context.getTag();
		Map<String, String> filterMethodMap = (Map<String, String>)tag.getFilterMethodMap();
		if (Collections.isEmpty(fm2) 
				&& Collections.isEmpty(filterMethodMap)) {
			return;
		}

		write("<tr class=\"n-lv-filters-sep\"><td colspan=\"3\"><div></div></td></tr>");

		write("<tr class=\"n-tr-submit\">");
		write("<td class=\"n-td-label\">");
		write(getText("listview-filters-label-method"));
		write(":</td>");
		write("<td class=\"n-td-input\" colspan=\"2\">");
		
		write("<table class=\"n-lv-fs-tb-options\"><tbody><tr>");
		if (Collections.isNotEmpty(filterMethodMap)) {
			write("<td>");
			writeRadio("n-lv-f-method", qfilterm,
					id + "_fsform_filterm", filterMethodMap, null);
			write("</td>");
		}
		if (Collections.isNotEmpty(fm2)) {
			write("<td>");
			write("<select id=\"" + id + "_fsform_fsadd" + "\"");
			write(" class=\"n-lv-fs-select\" onclick=\"return false;\" onchange=\"_nlv_onAddFilter(this)\">");
			write("<option value=\"\">-- ");
			write(getText("listview-filters-label-add"));
			write(" --</option>");
	
			for (Entry<String, Filter> en : fm2.entrySet()) {
				String _name = en.getKey();
				Filter _f = en.getValue();
	
				boolean _fd = _fsvl.contains(_name);
	
				write("<option value=\"" + html(_name) + "\"");
				if (_fd) {
					write(" disabled");
				}
				write(">");
				write(html(_f.label));
				write("</option>");
			}
			write("</select>");
			write("</td>");
		}
		write("</tr></table>");

		write("</td></tr>");
	}
	
	private void writeListViewFiltersButtons() throws IOException {
		write("<tr class=\"n-tr-submit\">");
		write("<td class=\"n-td-label\"></td>");
		write("<td class=\"n-td-submit\" colspan=\"2\">");
		write(button(getText("listview-filters-button-query"), "n-icon n-icon-magnifier"));
		write(button(getText("listview-filters-button-clear"), "n-icon n-icon-page_white_swoosh", null, "return _nlv_onClearFilters(this);"));
		write("</td></tr>");
	}

	private static class Filter {
		String type;
		boolean fixed = false;
		String label;
		String tooltip;
		Object list;
	}
	
	private void writeListViewFilters() throws IOException {
		Map<String, Filter> fm = new LinkedHashMap<String, Filter>();
		
		String _fs = (String)params.get("filters");
		Iterator itc = MakeIterator.convert(columns);
		while (itc.hasNext()) {
			Object c = itc.next();
			
			boolean filterable = getBeanProperty(c, "filterable", true);
			if (filterable) {
				Filter of = new Filter();
				Object filter = getBeanProperty(c, "filter");
				if (filter != null && !Boolean.FALSE.equals(getBeanProperty(filter, "display"))) {
					String cname = (String)getBeanProperty(c, "name");
					fm.put(cname, of);

					of.type = (String)getBeanProperty(filter, "type");
					of.fixed = Boolean.TRUE.equals(getBeanProperty(filter, "fixed"));
					of.label = (String)getBeanProperty(filter, "label");
					if (of.label == null) {
						of.label = (String)getBeanProperty(c, "header");
					}
					of.tooltip = (String)getBeanProperty(filter, "tooltip");
					if (of.tooltip == null) {
						of.tooltip = (String)getBeanProperty(c, "tooltip");
					}
					of.list = getBeanProperty(filter, "list");
					
					String _fn = _fs + '.' + cname;

					boolean _hfe = false;
					if (fieldErrors != null) {
						for (Entry<String, List<String>> en2 : fieldErrors.entrySet()) {
							if (en2.getKey().startsWith(_fn + '.')) {
								_hfe = true;
								break;
							}
						}
					}
					if (of.fixed || _hfe || Strings.isNotEmpty((String)findValue(_fn + ".c", String.class))) {
						_fsvl.add(cname);
					}

					
					boolean _fd = false;

					String _fv, _fv2;
					if ("string".equals(of.type)) {
						_fv = _fn + ".sv";
						_fd = Strings.isNotEmpty((String)findValue(_fv, String.class));
					}
					else if ("boolean".equals(of.type)) {
						_fv = _fn + ".bv";
						_fv2 = _fn + ".bv2";
						_fd = findValue(_fv) != null || findValue(_fv2) != null;
					}
					else if ("number".equals(of.type)) {
						_fv = _fn + ".nv";
						_fv2 = _fn + ".nv2";
						_fd = findValue(_fv) != null || findValue(_fv2) != null;
					}
					else if ("date".equals(of.type)
							|| "datetime".equals(of.type)
							|| "time".equals(of.type)) {
						_fv = _fn + ".dv";
						_fv2 = _fn + ".dv2";
						_fd = findValue(_fv) != null || findValue(_fv2) != null;
					}
					else if ("checklist".equals(of.type)) {
						_fv = _fn + ".svs";
						_fd = Collections.isNotEmpty((List)findValue(_fv));
					}
					else if ("radio".equals(of.type)
							|| "select".equals(of.type)) {
						_fv = _fn + ".sv";
						_fd = Strings.isNotEmpty((String)findValue(_fv, String.class));
					}
					
					if (_fd) {
						_fsdl.add(cname);
					}
				}
			}
		}
		
		if (Collections.isEmpty(fm)) {
			return;
		}
		
		write("<fieldset class=\"n-lv-filters\">");
		write("<legend>");
		write(getText("listview-filters-caption"));
		write("</legend>");

		Form form = new Form(stack,
				StrutsContextUtils.getServletRequest(),
				StrutsContextUtils.getServletResponse());

		StrutsContextUtils.getContainer(stack).inject(form);

		form.setId(id + "_fsform");
		form.setCssClass("n-lv-fsform" + (_fsvl.isEmpty() ? " n-hidden" : ""));
		form.setAction((String)params.get("action"));
		form.setMethod(defs((String)params.get("method"), "get"));
		form.setOnsubmit((String)params.get("onsubmit"));
		form.setOnreset((String)params.get("onreset"));
		form.setLoadmask("false");

		form.start(writer);

		if (Strings.isNotEmpty(sort)) {
			writeHidden(id + "_fsform_sort", sort);
		}
		if (Strings.isNotEmpty(dir)) {
			writeHidden(id + "_fsform_dir", dir);
		}
		if (Strings.isNotEmpty(limit)) {
			writeHidden(id + "_fsform_limit", limit);
		}

		write("<table class=\"n-lv-filters-t\" cellspacing=\"0\"><tbody>");
		writeListViewFiltersItems(fm);
		writeListViewFiltersOptions(fm);
		writeListViewFiltersButtons();

		write("</tbody></table>");
		
		form.end(writer, "");
		
		write("</fieldset>");

		writeJsc("$(function() { _nlv_init_filters('" + ids + "', " + _fsvl.isEmpty() + "); });");
	}

	private void writeListViewTableHeader() throws IOException {
		write("<thead class=\"n-lv-thead n-lv-chead\">");
		write("<tr class=\"n-lv-tr\">");

		Iterator itc = MakeIterator.convert(columns);
		while (itc.hasNext()) {
			Object c = itc.next();
			
			boolean display = getBeanProperty(c, "display", true);
			if (!display) {
				continue;
			}

			String cname = (String)getBeanProperty(c, "name");
			String ctype = defs((String)getBeanProperty(c, "type"), "column");
			boolean csortable = getBeanProperty(c, "sortable", false);
			boolean chidden = getBeanProperty(c, "hidden", false);
			Object cwidth = getBeanProperty(c, "width");
			String cheader = (String)getBeanProperty(c, "header");
			
			Attributes tha = new Attributes();
			tha.add("column", cname)
			   .add("class", "n-lv-" + ctype 
					   + ("column".equals(ctype) ? " n-lv-cm-" + cname : "")
					   + (sortable && csortable ? " n-lv-sortable" + uiClass("n-sortable") : "")
					   + (cname.equals(_sort) ? " n-sorted n-lv-sort-" + _dir : "")
					   + (_fsdl.contains(cname) ? " n-lv-filtered" : "")
					   + (chidden ? " n-lv-hidden" : ""))
			   .addIfExists("title", params.get("tooltip"));
			stag("th", tha);
			
			write("<div class=\"n-lv-cell\"");
			if (cwidth != null) {
				write(" style=\"width:" + cwidth + ";\"");
			}
			write(">");

			if ("check".equals(ctype)) {
				if (cheader != null) {
					write(cheader);
				}
				else {
					String sa = getText("tooltip-selectAll", "");
					String sn = getText("tooltip-selectNone", "");
					Attributes a = new Attributes();
					a.add("type", "checkbox")
					 .add("class", "checkbox n-lv-ca")
					 .add("title", sa)
					 .add("selectAll", sa)
					 .add("selectNone", sn);
					xtag("input", a);
				}
			}
			else {
				write(cheader);
			}
			
			if (cname.equals(_sort)) {
				write("<span class=\"ui-icon ui-icon-triangle-1-" 
						+ ("asc".equals(_dir) ? 'n' : 's')
						+ " n-lv-sort-" + _dir
						+ "\"></span>");
			}
			else if (sortable && csortable) {
				write("<span class=\"ui-icon ui-icon-triangle-2-n-s n-lv-sort-off\"></span>");
			}
			write("</div>");
			etag("th");
		}
		write("</tr></thead>");
	}

	private String getCssColumnClass(Object d) {
		StringBuilder ccc = new StringBuilder();//(StringUtils.isEmpty(_cc) ? "" : " n-lv-cc-" + _cc);
		for (String c : cssColumns) {
			Object v = getBeanProperty(d, c);
			if (v != null) {
				String s = Strings.strip(v.toString());
				if (Strings.isNotEmpty(s)) {
					ccc.append(" n-lv-cc-").append(html(c)).append('-').append(html(s));
				}
			}
		}
		return ccc.toString();
	}
	
	private void writeListViewTable() throws IOException {
		write("<div id=\"" + ids + "_body\" class=\"n-lv-body" + uiClass("n-table-wrapper") + "\">");
		
		Form form = new Form(stack,
				StrutsContextUtils.getServletRequest(),
				StrutsContextUtils.getServletResponse());

		StrutsContextUtils.getContainer(stack).inject(form);

		form.setId(id + "_bform");
		form.setCssClass("n-lv-bform");
		form.setAction((String)params.get("action"));
		form.setMethod(defs((String)params.get("method"), "post"));
		form.setTarget((String)params.get("target"));
		form.setOnsubmit((String)params.get("onsubmit"));
		form.setOnreset((String)params.get("onreset"));
		form.setLoadmask("false");

		form.start(writer);

		write((String)params.get("hiddens"));
		
		write("<table id=\"" + ids + "_table\" class=\"n-lv-table" + uiClass("n-table") + "\">");
		writeListViewTableHeader();
		
		Iterator list = MakeIterator.convert(params.get("list"));
		if (list != null && list.hasNext()) {
			write("<tbody class=\"n-lv-tbody\">");

			int inx = 0;
			Object prev_d = null;
			
			while (list.hasNext()) {
				Object d = list.next();

				String oe = (inx % 2 == 0 ? "odd" : "even");
				write("<tr class=\"n-lv-tr n-lv-tr-" + oe + uiClass("n-tr-" + oe)
						+ getCssColumnClass(d)
						+ "\">");

				Iterator itc = MakeIterator.convert(columns);
				while (itc.hasNext()) {
					Object c = itc.next();
					
					boolean display = getBeanProperty(c, "display", true);
					if (!display) {
						continue;
					}

					String cname = (String)getBeanProperty(c, "name");
					String ctype = defs((String)getBeanProperty(c, "type"), "column");
					boolean cfixed = getBeanProperty(c, "fixed", false);
					boolean chidden = getBeanProperty(c, "hidden", false);
					boolean cnowrap = getBeanProperty(c, "nowrap", false);
					boolean cgroup = getBeanProperty(c, "group", false);
					boolean cvalue = getBeanProperty(c, "value", true);
					boolean cpkey = getBeanProperty(c, "pkey", false);
					boolean cenabled = getBeanProperty(c, "enabled", false);
					Object cwidth = getBeanProperty(c, "width");
					
					Iterator ia = MakeIterator.convert(getBeanProperty(c, "actions"));
					
					Attributes tda = new Attributes();
					tda.add("class", "n-lv-" + ctype 
							   + ("column".equals(ctype) ? " n-lv-cm-" + cname : "")
							   + (chidden ? " n-lv-hidden" : "")
							   + (cnowrap ? " n-lv-nowrap" : ""));
					stag(cfixed ? "th" : "td", tda);
					
					write("<div class=\"n-lv-cell" + (cnowrap ? " n-lv-nowrap\"" : "\""));
					
					if (cwidth != null) {
						write(" style=\"width:" + cwidth + ";\"");
					}
					write(">");

					if ("rownum".equals(ctype)) {
						write(String.valueOf(inx + 1));
					}
					else if ("number".equals(ctype)) {
						write(String.valueOf(_start + inx + 1));
					}
					else if ("check".equals(ctype)) {
						write("<input type=\"checkbox\" class=\"checkbox n-lv-cb\" value=\"" + inx + "\"/>");
					}
					else if ("actions".equals(ctype)) {
						while (ia.hasNext()) {
							Object a = ia.next();
							if (writeAlink("n-lv-ia", d, a)) {
								String label = (String)getBeanProperty(a, "label");
								if (Strings.isNotEmpty(label)) {
									write("<span class=\"n-lv-t\">");
									write(label);
									write("</span>");
								}
								write("</a>");
							}
						}
					}
					else {
						if (cvalue) {
							Attributes ha = new Attributes();
							ha.add("type", "hidden")
							  .add("class", "n-lv-cv" + (cpkey ? " n-lv-ck" : ""))
							  .add("name", listn + "[" + inx + "]." + cname)
							  .addIfTrue("disabled", !cenabled)
							  .add("value", formatRawValue(getBeanProperty(d, cname)), false);
							xtag("input", ha);
						}
						
						if (!(cgroup && prev_d != null 
								&& defs(getBeanProperty(prev_d, cname)).equals(defs(getBeanProperty(d, cname))))) {
							Object clink = getBeanProperty(c, "link");
							if (clink instanceof Boolean && ((Boolean)clink).booleanValue()) {
								clink = _link;
							}
							if (clink instanceof Map) {
								writeAlink("n-lv-a", d, clink);
							}

							String cformat = null;

							Object oFormat = getBeanProperty(c, "format");
							if (oFormat != null) {
								if (oFormat instanceof String) {
									cformat = (String)oFormat;
								}
								else {
									cformat = (String)getBeanProperty(oFormat, "type");
								}
							}

							if ("code".equals(cformat)) {
								Object v = getBeanProperty(d, cname);
								Iterator iv = MakeIterator.convert(v);
								if (iv != null) {
									Object codemap = getBeanProperty(oFormat, "codemap");
									while (iv.hasNext()) {
										writeCodeText(codemap, iv.next());
										if (iv.hasNext()) {
											write(" ");
										}
									}
								}
							}
							else if ("expression".equals(cformat)) {
								try {
									stack.push(d);
									Object exp = getBeanProperty(oFormat, "expression");
									if (exp instanceof String) {
										Object v = findValue((String)exp);
										if (v != null) {
											String escape = (String)getBeanProperty(oFormat, "escape");
											write(formatRawValue(v, escape));
										}
									}
								}
								finally {
									stack.pop();
								}
							}
							else {
								try {
									stack.push(d);
									Property p = new Property(stack);
									try {
										p.setName(cname);
										p.setFormat(cformat);
										write(p.formatValue());
									}
									finally {
										p.getComponentStack().pop();
									}
								}
								finally {
									stack.pop();
								}
							}

							if (clink instanceof Map) {
								write("</a>");
							}
						}
					}
					
					write("</div>");
					etag(cfixed ? "th" : "td");
				}
				
				write("</tr>");
				prev_d = d;
				
				inx++;
			}
			write("</tbody>");
		}
		
		write("</table>");

		writeJsc("$(function() {"
				+ "_nlv_init_table('" + jsstr(id) + "', {"
				+ "autosize: " + Attributes.isTrue(params.get("autosize"), true)
				+ "}); });");

		form.end(writer, "");
		
		write("</div>");
	}
	
	@SuppressWarnings("unchecked")
	private void writeLinkUrl(Object link) throws IOException {
		URL url = new URL(stack,
				StrutsContextUtils.getServletRequest(),
				StrutsContextUtils.getServletResponse());

		StrutsContextUtils.getContainer(stack).inject(url);
		
		url.setAction((String)getBeanProperty(link, "action"));
		url.setNamespace((String)getBeanProperty(link, "namespace"));
		url.setValue((String)getBeanProperty(link, "url"));
		
		Map<String, String> params = (Map<String, String>)getBeanProperty(link, "params");
		if (params == null) {
			params = (Map<String, String>)getBeanProperty(_link, "params");
		}
		if (params != null) {
			for (Entry<String, String> en : params.entrySet()) {
				url.addParameter(en.getKey(), findValue(en.getValue()));
			}
		}

		url.start(writer);
		url.end(writer, "");
	}
	
	private boolean writeAlink(String cls, Object d, Object link) throws IOException {
		try {
			stack.push(d);
			
			if (_permit != null) {
				String action = (String)getBeanProperty(link, "action");
				if (Strings.isNotEmpty(action)) {
					if (!_permit.hasDataPermission(d, action)) {
						return false;
					}
				}
			}
			
			String icon = (String)getBeanProperty(link, "icon");
			String target = (String)getBeanProperty(link, "target");
			String tooltip = (String)getBeanProperty(link, "tooltip");
			String href = (String)getBeanProperty(link, "href");
			String onclick = (String)getBeanProperty(link, "onclick");

			write("<a class=\"");
			write(cls);
			write("\" href=\"");
			if (Strings.isNotEmpty(href)) {
				write(href);
			}
			else {
				writeLinkUrl(link);
			}
			write("\"");
			if (Strings.isNotEmpty(target)) {
				write(" target=\"" + html(target) + "\"");
			}
			if (Strings.isNotEmpty(tooltip)) {
				write(" title=\"" + html(tooltip) + "\"");
			}
			if (Strings.isNotEmpty(onclick)) {
				write(" onclick=\"" + (onclick) + "\"");
			}
			write(">");
			
			if (Strings.isNotEmpty(icon)) {
				write(xicon(icon + " n-lv-i"));
			}
			
			return true;
		}
		finally {
			stack.pop();
		}
	}
	
	private void writeCodeText(Object cm, Object v) throws IOException {
		if (cm instanceof String) {
			Map m = codemaps.get(cm);
			if (m == null) {
				m = (Map)findValue((String)cm, Map.class);
				codemaps.put((String)cm, m);
			}
			cm = m;
		}

		if (cm instanceof Map) {
			v = ((Map)cm).get(v);
			write(v == null ? null : v.toString()); 
		}
	}
}
