BasicModelManagerUnit.java

package eu.javaexperience.webgsdb.frontend;

import static eu.jvx.js.lib.bindings.H.H;
import eu.javaexperience.query.F;
import eu.javaexperience.query.LogicalGroup;
import hu.ddsi.java.database.GenericStorage;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.events.Event;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.xml.Element;

import eu.javaexperience.classes.ClassDescriptor;
import eu.javaexperience.classes.ClassFieldDescriptor;
import eu.javaexperience.classes.dinamic.DinamicModelManager;
import eu.javaexperience.collection.map.OneShotMap;
import eu.javaexperience.datareprez.DataObject;
import eu.javaexperience.datareprez.DataReprezTools;
import eu.javaexperience.functional.BoolFunctions;
import eu.javaexperience.gsdbrpc.WebDbModel;
import eu.javaexperience.gsdbrpc.WebGsdbTools;
import eu.javaexperience.gsdbrpc.api.ModelManager;
import eu.javaexperience.interfaces.simple.getBy.GetBy1;
import eu.javaexperience.interfaces.simple.getBy.GetBy2;
import eu.javaexperience.interfaces.simple.getBy.GetBy3;
import eu.javaexperience.interfaces.simple.getBy.GetBy4;
import eu.javaexperience.interfaces.simple.publish.SimplePublish1;
import eu.javaexperience.interfaces.simple.publish.SimplePublish2;
import eu.javaexperience.patterns.creational.builder.PublisherBuilder;
import eu.javaexperience.reflect.CastTo;
import eu.javaexperience.reflect.Mirror;
import eu.javaexperience.teasite.frontend.table.GeneratedResultTable;
import eu.javaexperience.teasite.frontend.table.ResultTableGenerator;
import eu.javaexperience.teasite.frontend.table.ResultTableGenerator.TableField;
import eu.javaexperience.verify.TranslationFriendlyValidationEntry;
import eu.javaexperience.verify.ValidationResult;
import eu.javaexperience.webgsdb.activity.ModelEditPage;
import eu.javaexperience.webgsdb.commons.FieldExtraAttributes;
import eu.javaexperience.webgsdb.commons.FrontendFieldEditContext;
import eu.javaexperience.webgsdb.test.WebGsdbFrontendTestTools;
import eu.jvx.js.lib.ImpersonalisedHtml;
import eu.jvx.js.lib.TeaVmTools;
import eu.jvx.js.lib.bindings.H;
import eu.jvx.js.lib.bindings.VanillaTools;
import eu.jvx.js.lib.bindings.VanillaTools.ClassList;
import eu.jvx.js.lib.style.StyleTools.StyleAlaCarte;
import eu.jvx.js.lib.style.TbsStyle;
import eu.jvx.js.lib.ui.FrontendTools;
import eu.jvx.js.lib.ui.component.func.HtmlDataContainer;
import eu.jvx.js.lib.ui.component.func.HtmlDataContainerTools;
import eu.jvx.js.lib.ui.component.table.SimpleTableStructureManager;
import eu.jvx.js.lib.ui.component.table.TableRow;
import eu.jvx.js.tbs.TbsGlyph;
import eu.jvx.js.tbs.ui.TbsLayoutTools;
import eu.jvx.js.tbs.ui.TbsTools;
import eu.jvx.js.tbs.ui.TbsLayoutTools.SimpleFormRow;
import eu.teasite.frontend.api.ApiClient;

public class BasicModelManagerUnit<B extends WebDbModel, D extends B> implements ImpersonalisedHtml
{
	public ClassDescriptor cls;
	public ModelManager<B> acc;
	public ApiClient api;
	
	public BasicModelManagerUnit(ClassDescriptor cls, ModelManager<B> acc, ApiClient api)
	{
		this.cls = cls;
		this.acc = acc;
		this.api = api;
		
		tableGenerator = new ResultTableGenerator<>();
		
		tableGenerator.getFields = new SimplePublish1<List<TableField<D>>>()
		{
			@Override
			public void publish(List<TableField<D>> a)
			{
				{
					TableField<D> f = new TableField<D>();
					f.fieldName = "available_operations_column";
					f.renderLabel = new GetBy1<HTMLElement, TableField<D>>()
					{
						@Override
						public HTMLElement getBy(TableField<D> a)
						{
							if(hasPermission("create") && BasicModelManagerUnit.this.showAdd)
							{
								return addNewModelButton;
							}
							return null;
						}
					};
					
					f.renderField = (x, b, c)-> generateOperations.getBy(BasicModelManagerUnit.this, getPermissions(), c, b);
					
					a.add(f);
				}
				
				{
					TableField<D> f = new TableField<D>();
					f.fieldName = "do";
					f.renderLabel = new GetBy1<HTMLElement, TableField<D>>()
					{
						@Override
						public HTMLElement getBy(TableField<D> a)
						{
							H ret = new H("div");
							if(showId)
							{
								ret.attrs("#text", "id");
							}
							return ret.getHtml();
						}
					};
					
					f.renderField = new GetBy3<HTMLElement, ResultTableGenerator.TableField<D>, TableRow, D>()
					{
						@Override
						public HTMLElement getBy(TableField<D> a, TableRow b, D c)
						{
							String id = String.valueOf(c.get("do"));
							H ret = new H("div").attrs("data-name", "do", "data-value", id);
							if(showId)
							{
								ret.attrs("#text", id);
							}
							return ret.getHtml();
						}
					};
					
					a.add(f);
				}
				
				List<? extends ClassFieldDescriptor> fds = cls.getAllField();
				
				for(ClassFieldDescriptor fd:fds)
				{
					if(Boolean.TRUE == selectField.getBy("show", fd))
					{
						if(!fd.isUserAccessible())
						{
							continue;
						}
						FieldExtraAttributes mfd = FieldExtraAttributes.parse(fd);
						if(null == mfd || mfd.userMaySee)
						{
							TableField<D> add = new TableField<D>();
							add.fieldName = fd.getName();
							add.getExtraDataMap().put("fd", fd);
							
							
							add.renderLabel = (f)->
							{
								String name = f.fieldName;
								if(null != translateFieldName)
								{
									name = translateFieldName.getBy(name);
								}
								return new H("span").attrs("#text", name).getHtml();
							};
							//CUSTOM_FIELD_NAME;
							add.renderField = (GetBy3) ModelTableGeneratorTools.DEFAULT_MODEL_FIELD_RENDERER;
							a.add(add);
						}
					}
				}
			}
		};
	}
	
	public GetBy2<Boolean, String, ClassFieldDescriptor> selectField = (a,b)->true;
	
	public boolean hasPermission(String perm)
	{
		return getPermissions().contains(perm);
	}
	
	protected Set<String> perms = null;
	
	public Set<String> getPermissions()
	{
		if(null == perms)
		{
			perms = acc.getPermissions();
		}
		return perms;
	}

	public HTMLElement container = null;
	
	public HTMLElement filterBox = new H("div").getHtml();
	public HTMLElement resultTableBox = new H("div").getHtml();
	
	protected HTMLElement addNewModelButton = new H("span").attrs("class", WebGsdbFrontendTestTools.CSS_BTN_NEW_MODEL).style
	(
		TbsGlyph.PLUS_SIGN,
		TbsStyle.BTN_SUCCESS
	)
	.onClick((e)->popupAddNewModel()).getHtml();
	
	protected D createNewModel()
	{
		try
		{
			return acc.createModel(cls);
		}
		catch(Exception e)
		{
			Mirror.propagateAnyway(e);
			return null;
		}
	}
	
	public GetBy1<String, String> txtCreateNew = (s)->"Create new "+s;
	
	public void popupAddNewModel()
	{
		popupWithDataAndTrigger
		(
			WebGsdbFrontendTestTools.CSS_CREATE_NEW_MODEL_FORM,
			null,
			createNewModel(),
			txtCreateNew.getBy(cls.getClassName()),
			new SimplePublish2<DataObject, SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>>>()
			{
				@Override
				public void publish(DataObject data, SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>> b)
				{
					ValidationResult<TranslationFriendlyValidationEntry> ret = acc.create(cls, data);
					b.publish(ret);
				}
			}
		);
	}
	
	public GetBy2<String, String, D> txtOp = (op, model)->
	{
		switch (op)
		{
			case "delete": return "Do you really want to delete: "+model.toString();
			case "modify": return "Modifying "+model.toString();
		}
		return "translate(op: "+op+", model: "+model+")";
	};
	
	public GetBy1<String, String> translate = (s)->
	{
		if(null != s)
		{
			switch (s)
			{
			case "btn_cancel": return "Cancel";
			case "btn_close": return "Close";
			case "btn_save": return "Save";
			case "btn_delete": return "Delete";
			case "op_deletion_failed": return "Deletion failed";
			case "title_confirm_deletion": return "Confirm deletion";
			}
		}
		
		return "translate("+s+")";
	};
	
	public void popupConfirmDelete(D model, TableRow row)
	{
		H content = H("div"); 
		
		content.addChilds(new H("div").attrs("#text", txtOp.getBy("delete", model)));
		
		H header = H("h4");
		header.attrs("#text", translate.getBy("title_confirm_deletion"));
		
		H footer = H("div");
		
		footer.addChilds(TbsTools.createModalCloseButton(translate.getBy("btn_cancel")));
		footer.addChilds(H("button").attrs("class", "op_delete", "#text", translate.getBy("btn_delete")).style(TbsStyle.BTN_DANGER));
		
		VanillaTools.bindListenerToArea
		(
			footer.getHtml(),
			"click",
			".op_delete",
			FrontendTools.wrapProcessEventWithThread
			(
				new EventListener<Event>()
				{
					@Override
					public void handleEvent(Event a)
					{
						ValidationResult<TranslationFriendlyValidationEntry> ret = acc.delete
						(
							cls,
							(Long) CastTo.Long.cast(model.get("do")),
							null
						);
						
						if(ret.valid)
						{
							TbsTools.closeModal(content.getHtml());
							VanillaTools.remove(row.getHtml());
						}
						else
						{
							TbsTools.modalMessage(translate.getBy("op_deletion_failed"), renderMessages(ret.reportEntries));
						}
					}
				}
			)
		);
		
		TbsTools.modal
		(
			header.getHtml(),
			content.getHtml(),
			footer.getHtml()
		);
	}
	
	public String renderMessages
	(
		Collection<TranslationFriendlyValidationEntry> reportEntries
	)
	{
		StringBuilder sb = new StringBuilder();
		for(TranslationFriendlyValidationEntry e:reportEntries)
		{
			sb.append(renderEntry(e));
		}
		return sb.toString();
	}
	
	public String renderEntry(TranslationFriendlyValidationEntry ent)
	{
		return ent.translationSymbol;
	}
	
	public void popupEdit(D model, TableRow row)
	{
		popupWithDataAndTrigger
		(
			WebGsdbFrontendTestTools.CSS_MODEL_EDIT_FORM,
			new OneShotMap<String, String>("do", model.get("do").toString()),
			model,
			txtOp.getBy("modify", model),
			new SimplePublish2<DataObject, SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>>>()
			{
				@Override
				public void publish(DataObject data, SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>> b)
				{
					ValidationResult<TranslationFriendlyValidationEntry> a = acc.update(cls, data);
					if(a.valid)
					{
						DataReprezTools.copyInto(model, data);
						model.set("lastModify", System.currentTimeMillis());
						ResultTableGenerator.updateRow(lastResultTable.fields, row, model);
					}
					
					b.publish(a);
				}
			}
		);
	}
	
	static
	{
		//urchin: never runs on frontend but the TeaVm optimizer don't know about that, and so it keep the related fields/methods
		if(!TeaVmTools.IS_FRONTEND)
		{
			HtmlDataContainer<String> c = HtmlDataContainerTools.browserDatetimeLocal(null);
			c.setData("");
		}
	}
	
/*	public void updateRowBy(TableRow row, DataLike obj)
	{
		for(TableField<D> k:lastResultTable.fields)
		{
			TableCell cell = row.getCellByName(k.fieldName);
			HTMLElement h = cell.getHtml();
			h.clear();
			HTMLElement el = k.renderField.getBy(k, row, (D) obj);
			if(null != el)
			{
				h.appendChild(el);
			}
		}
	}
*/	
	/**
	 * 
	 * TODO extra fileds (id),
	 * TODO custom value setting.
	 * TODO add validation error 
	 * 
	 * 
	 * TODO externalize to customizable class
	 */
	protected void popupWithDataAndTrigger
	(
		String extraCss,
		Map<String, String> extraAttributes,
		D model,
		//GetBy2<String, D, String> getModelField,
		String title,
		SimplePublish2<DataObject, SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>>> onSave
	)
	{
		H content = H("div");
		if(null != extraCss)
		{
			content.attrs("class", extraCss);
		}
		
		PublisherBuilder<SimpleFormRow, HTMLElement, Void> builder = TbsLayoutTools.buildForm();
		builder.initialize(null);
		
		if(null != extraAttributes)
		{
			for(Entry<String, String> kv:extraAttributes.entrySet())
			{
				builder.publish(new SimpleFormRow(null, null, H("input").attrs("type", "hidden", "name", kv.getKey(), "value", kv.getValue()).getHtml()));
			}
		}
		
		FrontendFieldEditContext ctx = new FrontendFieldEditContext<>();
		ctx.acc = acc;
		ctx.api = api;
		ctx.gdb = GenericStorage.getOwnerDatabase(model);
		ctx.model = model;
		
		List<ClassFieldDescriptor> fields = new ArrayList<>();
		
		for(ClassFieldDescriptor f:cls.getAllField())
		{
			if(Boolean.TRUE == selectField.getBy("update", f))
			{
				fields.add(f);
			}
		}
		
		WebGsdbTools.generateModelEditor
		(
			builder,
			fields,
			null == translateFieldName? fd->fd.getTranslationSymbol():fd->translateFieldName.getBy(fd.getName()),
			model,
			ctx
		);
		
		content.addChilds(builder.getResult());
		
		H header = H("h4");
		
		header.attrs("#text", title);
		
		H footer = H("div");
		
		footer.addChilds(TbsTools.createModalCloseButton(translate.getBy("btn_close")));
		footer.addChilds(H("button").attrs("class", "op_save", "#text", translate.getBy("btn_save")).style(TbsStyle.BTN_SUCCESS));
		
		VanillaTools.bindListenerToArea
		(
			footer.getHtml(),
			"click",
			".op_save",
			FrontendTools.wrapProcessEventWithThread
			(
				new EventListener<Event>()
				{
					@Override
					public void handleEvent(Event a)
					{
						DataObject data = FrontendTools.serializeInputsInArea(content.getHtml());
						onSave.publish(data, new SimplePublish1<ValidationResult<TranslationFriendlyValidationEntry>>()
						{
							@Override
							public void publish(ValidationResult<TranslationFriendlyValidationEntry> a)
							{
								if(a.valid)
								{
									TbsTools.closeModal(content.getHtml());
								}
								else
								{
									Window.alert("error");
									//TODO show valdation entries
								}
							}
						});
					}
				}
			)
		);
		
		TbsTools.modal
		(
			header.getHtml(),
			content.getHtml(),
			footer.getHtml()
		);
	}
	
	public GetBy4<HTMLElement, BasicModelManagerUnit<B, D>, Set<String>, D, TableRow> generateOperations = (table, permissions, model, row)->
	{
		return new H("div").addChilds
		(
			!permissions.contains("update")?null:new H("span").style(TbsGlyph.PENCIL, TbsStyle.BTN_DEFAULT, StyleAlaCarte.FS_1, StyleAlaCarte.MARGIN_1).onClick((e)->popupEdit(model, row)),
			!permissions.contains("delete")?null:new H("span").style(TbsGlyph.TRASH, TbsStyle.BTN_DEFAULT, StyleAlaCarte.FS_1, StyleAlaCarte.MARGIN_1).onClick((e)->popupConfirmDelete(model, row))
		).getHtml();		
	};
	
	protected static GetBy1 CUSTOM_FIELD_NAME = new GetBy1<HTMLElement, TableField>()
	{
		@Override
		public HTMLElement getBy(TableField a)
		{
			ClassFieldDescriptor fd = (ClassFieldDescriptor) a.getExtraDataMap().get("fd");
			String text = "-";
			if(null != fd)
			{
				text = fd.getTranslationSymbol();
				text = WebGsdbTools.translate(text);
			}
			
			return new H("span").attrs("#text", text).getHtml();
		}
	};
	
	public boolean showId = true;
	
	public ResultTableGenerator<D> tableGenerator;
	
	
	protected GeneratedResultTable<D> lastResultTable = null;
	
	protected GeneratedResultTable<D> renderTable(Iterable<D> arr)
	{
		GeneratedResultTable<D> ret = tableGenerator.generate(SimpleTableStructureManager.INSTANCE, arr);
		ClassList cl = VanillaTools.getClassList(ret.html);
		cl.add("table");
		cl.add("table-hover");
		cl.add("table-striped");
		cl.add("table-responsive");
		return ret;
	}
	
	protected SimplePublish1<List<D>> reRenderTable = new SimplePublish1<List<D>>()
	{
		@Override
		public void publish(List<D> a)
		{
			try
			{
				lastResultTable = renderTable(a);//ModelTableGeneratorTools.generateResultTable(cls, a, customizeResultTable);
				setContent(new H(lastResultTable.html));
			}
			catch (Throwable e)
			{
				e.printStackTrace();
			}
		}
	};
	
	public boolean showFilter = true;
	
	protected LogicalGroup filterCriteria = F.gt.is("do", -1);
	public boolean showAdd = true;
	public SimplePublish2<BasicModelManagerUnit<B, D>, List<D>> afterResultRendered = null;
	public GetBy1<String, String> translateFieldName;
	
	public void resetFilterCriteria(boolean apply)
	{
		setFilterCriteria(F.gt.is("do", -1), apply);
	}
	
	public void setFilterCriteria(LogicalGroup lg, boolean apply)
	{
		filterCriteria = lg;
		if(apply)
		{
			refresh();
		}
	}
	
	public LogicalGroup getFilterCriteria()
	{
		return filterCriteria;
	}
	
	public void refresh()
	{
		loadWithFilters(filterCriteria);
	}
	
	
	protected void loadWithFilters(LogicalGroup lg)
	{
		FrontendTools.runOnThread
		(
			()->
			{
				init();
				try
				{
					List<D> res = acc.select(cls, lg);
					reRenderTable.publish(res);
					if(null != afterResultRendered)
					{
						afterResultRendered.publish(this, res);
					}
				}
				catch (Exception e)
				{
					Mirror.propagateAnyway(e);
				}
			}
		);
	}
	
	public void reset()
	{
		container = null;
	}
	
	public void init()
	{
		if(null != container)
		{
			return;
		}

		filterBox = new H("div").addChilds
		(
			!showFilter?null:new H("button").style(TbsGlyph.REFRESH, TbsStyle.BTN_DEFAULT, StyleAlaCarte.FS_1, StyleAlaCarte.MARGIN_1).onClick((e)->refresh()).getHtml()
		).getHtml();
		
		container = null;
		container = new H("div").addChilds
		(
			filterBox,
			resultTableBox
		).getHtml();
	}
	
	@Override
	public Object getImpersonator()
	{
		return this;
	}

	@Override
	public Element getHtml()
	{
		if(null == container)
		{
			init();
		}
		return container;
	}

	public static BasicModelManagerUnit createEditorForClass(ApiClient api, String namespace, String cls)
	{
		return createEditorForClass(ModelEditPage.createModelManager(api, namespace), api, cls);
	}
	
	public static BasicModelManagerUnit createEditorForClass(DinamicModelManager modelManager, ApiClient api, String cls)
	{
		List<ClassDescriptor> models = modelManager.getManagedModels();
		for(ClassDescriptor model:models)
		{
			if(model.getClassName().equals(cls))
			{
				return new BasicModelManagerUnit<>(model, modelManager, api);
			}
		}
		
		throw new RuntimeException("No manager class found with name: "+cls);
	}

	public void setContent(H content)
	{
		resultTableBox.clear();
		new H(resultTableBox).addChilds(content);
	}

	public ModelManager<B> getDatabase()
	{
		return acc;
	}
}