/*
 Storybook: Open Source software for novelists and authors.
 Copyright (C) 2008 - 2015 Martin Mustun, Pete Keller

 This program 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, 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 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package storybook.model;

import i18n.I18N;
import ideabox.IdeaxFrm;
import java.awt.Component;
import java.awt.event.ActionListener;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import org.apache.commons.beanutils.BeanUtils;
import static org.apache.commons.beanutils.BeanUtils.copyProperties;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.converters.DateConverter;
import org.apache.commons.beanutils.converters.IntegerConverter;
import org.apache.commons.beanutils.converters.LongConverter;
import org.apache.commons.beanutils.converters.SqlTimestampConverter;
import org.apache.commons.beanutils.converters.StringConverter;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.w3c.dom.Element;
import resources.icons.ICONS;
import resources.icons.IconUtil;
import storybook.App;
import storybook.ctrl.Ctrl;
import storybook.exim.exporter.ExportBookToDoc;
import storybook.model.book.Book;
import static storybook.model.book.Book.TYPE.SCENE;
import storybook.model.handler.AbstractEntityHandler;
import storybook.model.handler.AttributeHandler;
import storybook.model.handler.CategoryHandler;
import storybook.model.handler.ChapterHandler;
import storybook.model.handler.EventHandler;
import storybook.model.handler.GenderHandler;
import storybook.model.handler.IdeaHandler;
import storybook.model.handler.InternalHandler;
import storybook.model.handler.ItemHandler;
import storybook.model.handler.ItemlinkHandler;
import storybook.model.handler.LocationHandler;
import storybook.model.handler.MemoHandler;
import storybook.model.handler.PartHandler;
import storybook.model.handler.PersonHandler;
import storybook.model.handler.PlotHandler;
import storybook.model.handler.RelationHandler;
import storybook.model.handler.SceneHandler;
import storybook.model.handler.StrandHandler;
import storybook.model.handler.TagHandler;
import storybook.model.handler.TaglinkHandler;
import storybook.model.hbn.dao.AttributeDAO;
import storybook.model.hbn.dao.CategoryDAO;
import storybook.model.hbn.dao.ChapterDAO;
import storybook.model.hbn.dao.EndnoteDAO;
import storybook.model.hbn.dao.EpisodeDAO;
import storybook.model.hbn.dao.EventDAO;
import storybook.model.hbn.dao.GenderDAO;
import storybook.model.hbn.dao.IdeaDAO;
import storybook.model.hbn.dao.InternalDAO;
import storybook.model.hbn.dao.ItemDAO;
import storybook.model.hbn.dao.ItemlinkDAO;
import storybook.model.hbn.dao.LocationDAO;
import storybook.model.hbn.dao.MemoDAO;
import storybook.model.hbn.dao.PartDAO;
import storybook.model.hbn.dao.PersonDAO;
import storybook.model.hbn.dao.PlotDAO;
import storybook.model.hbn.dao.RelationDAO;
import storybook.model.hbn.dao.SceneDAO;
import storybook.model.hbn.dao.StrandDAO;
import storybook.model.hbn.dao.TagDAO;
import storybook.model.hbn.dao.TaglinkDAO;
import storybook.model.hbn.entity.AbstractEntity;
import storybook.model.hbn.entity.AbstractTag;
import storybook.model.hbn.entity.Attribute;
import storybook.model.hbn.entity.Category;
import storybook.model.hbn.entity.Chapter;
import storybook.model.hbn.entity.Endnote;
import storybook.model.hbn.entity.Episode;
import storybook.model.hbn.entity.Event;
import storybook.model.hbn.entity.Gender;
import storybook.model.hbn.entity.Idea;
import storybook.model.hbn.entity.Item;
import storybook.model.hbn.entity.Itemlink;
import storybook.model.hbn.entity.Location;
import storybook.model.hbn.entity.Memo;
import storybook.model.hbn.entity.Part;
import storybook.model.hbn.entity.Person;
import storybook.model.hbn.entity.Plot;
import storybook.model.hbn.entity.Relationship;
import storybook.model.hbn.entity.Scene;
import storybook.model.hbn.entity.Scenes;
import storybook.model.hbn.entity.Status;
import storybook.model.hbn.entity.Strand;
import storybook.model.hbn.entity.Tag;
import storybook.model.hbn.entity.Taglink;
import storybook.tools.DateUtil;
import storybook.tools.LOG;
import storybook.tools.ListUtil;
import storybook.tools.TextUtil;
import storybook.tools.html.Html;
import storybook.tools.net.Net;
import static storybook.tools.swing.SwingUtil.showModalDialog;
import storybook.ui.MainFrame;
import storybook.ui.MainMenu;
import storybook.ui.dialog.ConfirmDeleteDlg;
import storybook.ui.renderer.lcr.LCREntity;

/**
 * utilities for Entity
 *
 * @author favdb
 */
public class EntityUtil {

	private static final String TT = "EntityUtil",
	   HTML_P_START = "<p style=\"padding-top:10px\">";
	public static boolean WITH_CHRONO = true;

	/**
	 * utility class
	 */
	private EntityUtil() {
		// empty
	}

	/**
	 * create an entity in the database
	 *
	 * @param mainFrame
	 * @param entity
	 */
	public static void createEntity(MainFrame mainFrame, AbstractEntity entity) {
		Session session = mainFrame.getBookModel().beginTransaction();
		session.save(entity);
		session.getTransaction().commit();
		if (session.isOpen()) {
			session.close();
		}
		mainFrame.getBookModel().fireAgain();
	}

	/**
	 * get the first entity of the given type
	 *
	 * @param mainFrame
	 * @param type
	 * @return
	 */
	public static AbstractEntity getFirst(MainFrame mainFrame, Book.TYPE type) {
		List entities = findEntities(mainFrame, type);
		if (entities.isEmpty()) {
			return null;
		}
		return (AbstractEntity) entities.get(0);
	}

	/**
	 * get the last entity of the given type
	 *
	 * @param mainFrame
	 * @param type
	 * @return
	 */
	public static AbstractEntity getLast(MainFrame mainFrame, Book.TYPE type) {
		List entities = findEntities(mainFrame, type);
		if (entities.isEmpty()) {
			return null;
		}
		return (AbstractEntity) entities.get(entities.size() - 1);
	}

	/**
	 * remove an entity from the database
	 *
	 * @param mainFrame
	 * @param type
	 * @param id
	 */
	public static void removeEntity(MainFrame mainFrame, Book.TYPE type, long id) {
		AbstractEntity entity = findEntityById(mainFrame, type, id);
		mainFrame.getBookModel().ENTITY_Delete(entity);
	}

	/**
	 * join entities
	 *
	 * @param mainFrame
	 * @param entities
	 */
	public static void joinEntities(MainFrame mainFrame, List<AbstractEntity> entities) {
		switch (entities.get(0).getObjType()) {
			case CHAPTER:
				joinChapters(mainFrame, entities);
				break;
			case SCENE:
				joinScenes(mainFrame, entities);
				break;
		}
	}

	/**
	 * join Chapters in one and remove joined chapters
	 *
	 * @param mainFrame
	 * @param chapters
	 */
	public static void joinChapters(MainFrame mainFrame, List<AbstractEntity> chapters) {
		//LOG.trace(TT + ".joinChapters(mainFrame, chapters nb=" + chapters.size() + ")");
		mainFrame.cursorSetWaiting();
		Chapter master = (Chapter) chapters.get(0);
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		SceneDAO dao = new SceneDAO(session);
		for (int i = 1; i < chapters.size(); i++) {
			Chapter chapter = (Chapter) chapters.get(i);
			session.refresh(chapter);
			List<Scene> scenes = dao.findByChapter(chapter);
			for (Scene scene : scenes) {
				scene.setChapter(master);
				session.update(scene);
			}
			session.delete(chapter);
		}
		model.commit();
		model.fireAgainScenes();
		model.fireAgainChapters();
		model.fireAgainParts();
		mainFrame.setUpdated();
		mainFrame.cursorSetDefault();
	}

	/**
	 * join summary of Scenes and delete joined scenes, joined text limited to 32700 chars max
	 *
	 * @param mainFrame
	 * @param scenes
	 */
	public static void joinScenes(MainFrame mainFrame, List<AbstractEntity> scenes) {
		//LOG.trace(TT + ".joinScenes(mainFrame, scenes nb=" + scenes.size() + ")");
		mainFrame.cursorSetWaiting();
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		Scene master = (Scene) scenes.get(0);
		session.refresh(master);
		StringBuilder text = new StringBuilder(master.getSummary());
		int len = master.getSummary().length();
		for (int i = 1; i < scenes.size(); i++) {
			Scene scene = (Scene) scenes.get(i);
			session.refresh(scene);
			if (len + scene.getSummary().length() < 32700) {
				text.append(scene.getSummary());
				addEntities(master.getPersons(), scene.getPersons());
				addEntities(master.getLocations(), scene.getLocations());
				addEntities(master.getItems(), scene.getItems());
				addEntities(master.getPlots(), scene.getPlots());
				if (!master.getStrands().contains(scene.getStrand())) {
					master.getStrands().add(scene.getStrand());
				}
				addEntities(master.getStrands(), scene.getStrands());
				session.delete(scene);
			} else {
				master.setSummary(text.toString());
				session.update(master);
				master = scene;
				text = new StringBuilder(master.getSummary());
				len = master.getSummary().length();
			}
		}
		master.setSummary(text.toString());
		session.update(master);
		model.commit();
		model.fireAgainScenes();
		model.fireAgainChapters();
		model.fireAgainParts();
		mainFrame.setUpdated();
		mainFrame.cursorSetDefault();
	}

	/**
	 * add entities list to the given master list
	 *
	 * @param master
	 * @param toadd
	 */
	@SuppressWarnings("unchecked")
	public static void addEntities(List master, List toadd) {
		if (!toadd.isEmpty()) {
			for (Object p : toadd) {
				if (!master.contains(p)) {
					master.add(p);
				}
			}
		}
	}

	/**
	 * create an entity from a given XML node
	 *
	 * @param mainFrame
	 * @param node
	 * @return
	 */
	public static boolean fromXml(MainFrame mainFrame, Element node) {
		boolean rc = false;
		Book.TYPE type = Book.getTYPE(node.getNodeName());
		AbstractEntity entity = null;
		switch (type) {
			case ATTRIBUTE:
				// forbidden
				break;
			case CATEGORY:
				entity = Category.fromXml(node);
				break;
			case CHAPTER:
				entity = Chapter.fromXml(node);
				break;
			case EVENT:
				entity = Event.fromXml(node);
				break;
			case GENDER:
				// forbidden
				break;
			case ITEM:
				entity = Item.fromXml(node);
				break;
			case ITEMLINK:
				// forbidden
				break;
			case LOCATION:
				entity = Location.fromXml(node);
				break;
			case PART:
				entity = Scene.fromXml(node);
				break;
			case PERSON:
				entity = Scene.fromXml(node);
				break;
			case PLOT:
				entity = Scene.fromXml(node);
				break;
			case RELATION:
				// forbidden
				break;
			case SCENE:
				entity = Scene.fromXml(node);
				break;
			case STRAND:
				entity = Strand.fromXml(node);
				break;
			case TAG:
				entity = Tag.fromXml(node);
				break;
			case TAGLINK:
				// forbidden
				break;
		}
		if (entity != null) {
			mainFrame.getBookController().newEntity(entity);
			rc = true;
		}
		return rc;
	}

	/**
	 * get list of ID which cannot be modified/removed
	 *
	 * @param entity
	 * @return
	 */
	public static List<Long> getReadOnlyIds(AbstractEntity entity) {
		ArrayList<Long> ret = new ArrayList<>();
		if (entity instanceof Category || entity instanceof Gender) {
			ret.add(1L);
			ret.add(2L);
		} else if (entity instanceof Part || entity instanceof Strand) {
			ret.add(1L);
		}
		return ret;
	}

	/**
	 * delete an Entity
	 *
	 * @param mainFrame
	 * @param entity to delete
	 */
	public static void delete(MainFrame mainFrame, AbstractEntity entity) {
		// check if read only
		boolean allowed = true;
		List<Long> readOnlyIds = EntityUtil.getReadOnlyIds(entity);
		if (readOnlyIds.contains(entity.getId())) {
			allowed = false;
		}
		if (entity.getObjType() == Book.TYPE.STRAND
		   && Book.getNbStrands(mainFrame) == 1) {
			allowed = false;
		}
		if (entity.getObjType() == Book.TYPE.PART
		   && Book.getNbParts(mainFrame) == 1) {
			allowed = false;
		}
		if (entity.getObjType() == Book.TYPE.CHAPTER
		   && Book.getNbChapters(mainFrame) == 1) {
			allowed = false;
		}
		if (!allowed) {
			JOptionPane.showMessageDialog(mainFrame,
			   I18N.getMsg("no.delete"),
			   I18N.getMsg("warning"),
			   JOptionPane.ERROR_MESSAGE);
			return;
		}
		ConfirmDeleteDlg dlg = new ConfirmDeleteDlg(mainFrame, entity);
		showModalDialog(dlg, mainFrame, true);
		if (dlg.isCanceled()) {
			return;
		}
		Ctrl ctrl = mainFrame.getBookController();
		Part lastUsed = mainFrame.lastPartGet();
		if (entity instanceof Part
		   && (lastUsed != null && lastUsed.getId().equals(entity.getId()))) {
			// current part will be delete, change to first part
			Model model = mainFrame.getBookModel();
			Session session = model.beginTransaction();
			PartDAO dao = new PartDAO(session);
			Part firstPart = dao.findFirst();
			session.close();
			mainFrame.lastPartSet(firstPart);
		}
		ctrl.deleteEntity(entity);
	}

	/**
	 * delete links for Tag/Item in linked Entity
	 *
	 * @param model
	 * @param entity
	 */
	public static void deleteTagAndItemlinks(Model model, AbstractEntity entity) {
		Session session = model.beginTransaction();
		TaglinkDAO tagLinkDao = new TaglinkDAO(session);
		List<Taglink> tagLinks = null;
		if (entity instanceof Scene) {
			tagLinks = tagLinkDao.findByStartOrEndScene((Scene) entity);
		} else if (entity instanceof Person) {
			tagLinks = tagLinkDao.findByPerson((Person) entity);
		} else if (entity instanceof Location) {
			tagLinks = tagLinkDao.findByLocation((Location) entity);
		}
		session.close();
		if (tagLinks != null && !tagLinks.isEmpty()) {
			for (Taglink link : tagLinks) {
				model.TAGLINK_Delete(link);
			}
		}
		session = model.beginTransaction();
		ItemlinkDAO itemLinkDao = new ItemlinkDAO(session);
		List<Itemlink> itemLinks = null;
		if (entity instanceof Scene) {
			itemLinks = itemLinkDao.findByStartOrEndScene((Scene) entity);
		} else if (entity instanceof Person) {
			itemLinks = itemLinkDao.findByPerson((Person) entity);
		} else if (entity instanceof Location) {
			itemLinks = itemLinkDao.findByLocation((Location) entity);
		}
		session.close();
		if (itemLinks != null && !itemLinks.isEmpty()) {
			for (Itemlink link : itemLinks) {
				model.ITEMLINK_Delete(link);
			}
		}

	}

	/**
	 * copy properties from a given entity to a given entity
	 *
	 * @param mainFrame
	 * @param inEntity
	 * @param toEntity
	 */
	public static void copyEntityProperties(MainFrame mainFrame,
	   AbstractEntity inEntity,
	   AbstractEntity toEntity) {
		try {
			if (inEntity == null || toEntity == null) {
				LOG.err("EntityUtils.copyEntityProperties inEntity or toEntity is null");
				return;
			}
			ConvertUtils.register(new DateConverter(null), Date.class);
			ConvertUtils.register(new SqlTimestampConverter(null), Timestamp.class);
			ConvertUtils.register(new IntegerConverter(), Integer.class);
			ConvertUtils.register(new LongConverter(), Long.class);
			ConvertUtils.register(new StringConverter(), String.class);
			copyProperties(toEntity, inEntity);
		} catch (IllegalAccessException | InvocationTargetException e) {
			if (e.getMessage() == null) {
				return;
			}
			LOG.err(e.getMessage(), e);
		}
	}

	/**
	 * clone a given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @return
	 */
	public static AbstractEntity cloneEntity(MainFrame mainFrame, AbstractEntity entity) {
		if (entity != null) try {
			ConvertUtils.register(new DateConverter(null), Date.class);
			ConvertUtils.register(new SqlTimestampConverter(null), Timestamp.class);
			ConvertUtils.register(new IntegerConverter(), Integer.class);
			ConvertUtils.register(new StringConverter(), String.class);
			return (AbstractEntity) BeanUtils.cloneBean(entity);
		} catch (IllegalAccessException
		   | InstantiationException
		   | InvocationTargetException
		   | NoSuchMethodException e) {
			LOG.err("EntityUtil.cloneEntityProperties() Exception : ", e);
		}
		return null;
	}

	/**
	 * copy a given Entity into a new Entity
	 *
	 * @param mainFrame
	 * @param inEntity
	 */
	public static void copyEntity(MainFrame mainFrame, AbstractEntity inEntity) {
		if (inEntity == null) {
			return;
		}
		AbstractEntity toEntity = createNewEntity(inEntity);
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		session.refresh(inEntity);
		copyEntityProperties(mainFrame, inEntity, toEntity);
		markCopiedEntity(toEntity);
		List<Plot> plots = new ArrayList<>();
		List<Item> items = new ArrayList<>();
		List<Location> locations = new ArrayList<>();
		List<Person> persons = new ArrayList<>();
		List<Strand> strands = new ArrayList<>();
		switch (Book.getTYPE(inEntity)) {
			case SCENE:
				//empty all links to others
				Scene scene = (Scene) inEntity;
				Scene newScene = (Scene) toEntity;
				if (newScene == null) {
					break;
				}
				if (!scene.hasRelativescene()) {
					newScene.removeRelativescene();
				}
				newScene.setPersons(persons);
				newScene.setLocations(locations);
				newScene.setItems(items);
				newScene.setPlots(plots);
				newScene.setStrands(strands);
				if (!scene.hasRelativescene()) {
					newScene.removeRelativescene();
				}
				break;
			default:
				break;
		}
		model.commit();
		Ctrl ctrl = mainFrame.getBookController();
		ctrl.newEntity(toEntity);
	}

	/**
	 * change the Entity name to add (copy) for Chapter, Scene, Strand, Tag, Idea
	 *
	 * @param mainFrame
	 * @param entity
	 */
	private static void markCopiedEntity(AbstractEntity entity) {
		String copyStr = "(" + I18N.getMsg("copy") + ") ";
		switch (Book.getTYPE(entity)) {
			case SCENE:
				Scene s = (Scene) entity;
				s.setTitle(copyStr + s.getTitle());
				break;
			case CHAPTER:
				Chapter c = (Chapter) entity;
				c.setTitle(copyStr + c.getName());
				break;
			case ATTRIBUTE:
			case CATEGORY:
			case GENDER:
			case ITEM:
			case LOCATION:
			case PART:
			case PLOT:
			case RELATION:
			case STRAND:
			case TAG:
				AbstractTag t = (AbstractTag) entity;
				t.setName(copyStr + t.getName());
				break;
			case IDEA:
				Idea i = (Idea) entity;
				i.setName(copyStr + i.getName());
				i.setNotes(copyStr + i.getNotes());
				break;
			default:
				break;
		}
	}

	/**
	 * create a JPopupMenu for the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @param withChrono : boolean true i view chrono is required
	 *
	 * @return
	 */
	public static JPopupMenu createPopupMenu(MainFrame mainFrame, AbstractEntity entity,
	   boolean withChrono) {
		JPopupMenu menu = new JPopupMenu();
		if (entity == null) {
			return null;
		}
		if (entity.isTransient()) {
			return null;
		}
		Ctrl ctrl = mainFrame.getBookController();
		menu.add(MainMenu.initMenuItem(ICONS.K.EDIT,
		   "edit", "", ' ',
		   "edit",
		   e -> mainFrame.showEditorAsDialog(entity)));
		if (Book.getTYPE(entity) == Book.TYPE.ENDNOTE) {
			return menu;
		}
		if (entity instanceof Scene && mainFrame.getBook().isUseXeditor()) {
			JMenuItem item = MainMenu.initMenuItem(ICONS.K.EMPTY,
			   "xeditor", "", ' ',
			   "xeditor",
			   e -> mainFrame.showXeditor(entity));
			item.setEnabled(new File(((Scene) entity).getOdf()).exists());
			menu.add(item);
		}
		menu.add(new JPopupMenu.Separator());
		// show in view...
		List<JMenuItem> lj = new ArrayList<>();
		if (entity instanceof Scene || entity instanceof Chapter) {
			if (withChrono) {
				lj.add(MainMenu.initMenuItem("view.chrono", "",
				   e -> ctrl.chronoShow(entity)));
			}
			lj.add(MainMenu.initMenuItem("view.book", "",
			   e -> ctrl.bookShow(entity)));
			lj.add(MainMenu.initMenuItem("view.manage", "",
			   e -> ctrl.manageShow(entity)));
		}
		lj.add(MainMenu.initMenuItem("view.info", "",
		   e -> ctrl.infoShow(entity)));
		if (isAvailableInMemoria(entity)) {
			lj.add(MainMenu.initMenuItem("view.memoria", "",
			   e -> ctrl.memoriaShow(entity)));
		}
		if (lj.size() > 1) {
			JMenu mj = new JMenu(I18N.getMsg("show.in"));
			mj.setIcon(IconUtil.getIconSmall(ICONS.K.SEARCH));
			for (JMenuItem lmj : lj) {
				mj.add(lmj);
			}
			menu.add(mj);
		} else {
			menu.add(lj.get(0));
		}
		menu.add(new JPopupMenu.Separator());
		if (entity instanceof Chapter) {
			menu.add(MainMenu.initMenuItem(ICONS.K.SORT, "order.time", "", ' ', "order.time",
			   e -> Scenes.renumberByTime(mainFrame, (Chapter) entity)));
			menu.add(MainMenu.initMenuItem(ICONS.K.SORT, "re_sort", "", ' ', "re_sort",
			   e -> Scenes.renumber(mainFrame, (Chapter) entity)));
			menu.add(new JPopupMenu.Separator());
		}

		if (entity instanceof Scene) {
			menu.add(Status.getMenu(mainFrame, (Scene) entity));
		}

		menu.add(MainMenu.initMenuItem(ICONS.K.NEW,
		   "new.entity", "", ' ', "new." + entity.getObjType().toString(),
		   e -> {
			   AbstractEntity n = EntityUtil.createNewEntity(entity);
			   if (n != null) {
				   ctrl.setEditEntity(n);
			   }
		   }));
		menu.add(MainMenu.initMenuItem(ICONS.K.DELETE, "delete", "", ' ', "delete",
		   e -> EntityUtil.delete(mainFrame, entity)));

		if (entity instanceof Idea) {
			JMenuItem item = new JMenuItem(I18N.getMsg("ideabox.copy_to"));
			item.setIcon(IconUtil.getIconSmall(ICONS.K.IDEABOX));
			item.addActionListener(e -> {
				Idea idea = (Idea) entity;
				if (IdeaxFrm.ideaboxAdd(mainFrame, idea) != null) {
					mainFrame.getBookModel().IDEA_Update(idea);
				}
			});
			menu.add(item);
		}
		if (entity instanceof Location) {
			menu.add(new JPopupMenu.Separator());
			menu.add(MainMenu.initMenuItem(ICONS.K.WORLD, "openStreetMap", "", ' ', "!OpenStreetMap",
			   e -> Net.openOSM((Location) entity)));
		}
		if (entity instanceof Part || entity instanceof Chapter) {
			JMenu lexp = new JMenu(I18N.getMsg("export.tips"));
			if ((entity instanceof Part && EntityUtil.findEntities(mainFrame, Book.TYPE.PART).size() > 1)
			   || entity instanceof Chapter) {
				lexp.add(MainMenu.initMenuItem(ICONS.K.EMPTY, "docx", "", ' ', "file.type.docx",
				   e -> ExportBookToDoc.createPartial(mainFrame, entity, "docx")));
				lexp.add(MainMenu.initMenuItem(ICONS.K.EMPTY, "odt", "", ' ', "file.type.odt",
				   e -> ExportBookToDoc.createPartial(mainFrame, entity, "odt")));
			}
			/*else if (entity instanceof Chapter) {
				lexp.add(MainMenu.initMenuItem(ICONS.K.EMPTY, "docx", "", ' ', "file.type.docx",
				   e -> ExportBookToDoc.createPartial(mainFrame, (Chapter) entity, "docx")));
				lexp.add(MainMenu.initMenuItem(ICONS.K.EMPTY, "odt", "", ' ', "file.type.odt",
				   e -> ExportBookToDoc.createPartial(mainFrame, (Chapter) entity, "odt")));
			}*/
			if (lexp.getItemCount() > 0) {
				menu.add(lexp);
			}
		}
		if (menu.getComponents().length == 0) {
			return null;
		}
		for (Component comp : menu.getComponents()) {
			if (comp instanceof JMenuItem) {
				((JMenuItem) comp).setFont(App.fonts.defGet());
			}
		}
		return menu;
	}

	/**
	 * check if the given Entity is available in Memoria
	 *
	 * @param entity
	 * @return
	 */
	public static boolean isAvailableInMemoria(AbstractEntity entity) {
		return entity instanceof Item
		   || entity instanceof Location
		   || entity instanceof Person
		   || entity instanceof Plot
		   || entity instanceof Scene
		   || entity instanceof Tag;
	}

	/**
	 * get the handler for the given entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @return
	 */
	public static AbstractEntityHandler handlerGet(MainFrame mainFrame, AbstractEntity entity) {
		switch (Book.getTYPE(entity)) {
			case ATTRIBUTE:
				return new AttributeHandler(mainFrame);
			case CATEGORY:
				return new CategoryHandler(mainFrame);
			case CHAPTER:
				return new ChapterHandler(mainFrame);
			case EVENT:
				return new EventHandler(mainFrame);
			case GENDER:
				return new GenderHandler(mainFrame);
			case IDEA:
				return new IdeaHandler(mainFrame);
			case INTERNAL:
				return new InternalHandler(mainFrame);
			case ITEM:
				return new ItemHandler(mainFrame);
			case ITEMLINK:
				return new ItemlinkHandler(mainFrame);
			case LOCATION:
				return new LocationHandler(mainFrame);
			case MEMO:
				return new MemoHandler(mainFrame);
			case PART:
				return new PartHandler(mainFrame);
			case PERSON:
				return new PersonHandler(mainFrame);
			//case PHOTO: return new PhotoHandler(mainFrame);
			case PLOT:
				return new PlotHandler(mainFrame);
			case RELATION:
				return new RelationHandler(mainFrame);
			case SCENARIO:
			/*if (mainFrame.getBook().isScenario()) {
					return new ScenarioHandler(mainFrame);
				}*/
			case SCENE:
				return new SceneHandler(mainFrame);
			case STRAND:
				return new StrandHandler(mainFrame);
			case TAG:
				return new TagHandler(mainFrame);
			case TAGLINK:
				return new TaglinkHandler(mainFrame);
			default:
				return null;
		}
	}

	/**
	 * get a list of JCheckBox for Categories
	 *
	 * @param mainFrame
	 * @param comp
	 * @return
	 */
	public static List<JCheckBox> categoryCreateCbList(MainFrame mainFrame, ActionListener comp) {
		List<JCheckBox> list = new ArrayList<>();
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		CategoryDAO dao = new CategoryDAO(session);
		List<Category> categories = dao.findAllOrderBySort();
		model.commit();
		for (Category category : categories) {
			JCheckBox cb = new JCheckBox(category.getName());
			cb.putClientProperty("CbCategory", category);
			cb.setOpaque(false);
			cb.addActionListener(comp);
			cb.setSelected(true);
			list.add(cb);
		}
		return list;
	}

	/**
	 * get a list of JCheckBox for countries
	 *
	 * @param mainFrame
	 * @param comp
	 * @return
	 */
	public static List<JCheckBox> countryCreateCbList(MainFrame mainFrame, ActionListener comp) {
		List<JCheckBox> list = new ArrayList<>();
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		LocationDAO dao = new LocationDAO(session);
		List<String> countries = dao.findCountries();
		model.commit();
		for (String country : countries) {
			JCheckBox chb = new JCheckBox(country);
			chb.setOpaque(false);
			chb.addActionListener(comp);
			chb.setSelected(true);
			list.add(chb);
		}
		return list;
	}

	/**
	 * get a list of JCheckBox for Item Categories
	 *
	 * @param mainFrame
	 * @param comp
	 * @return
	 */
	public static List<JCheckBox> itemCategoryCreateCbList(MainFrame mainFrame, ActionListener comp) {
		List<JCheckBox> list = new ArrayList<>();
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		ItemDAO dao = new ItemDAO(session);
		List<String> categories = dao.findCategories();
		model.commit();
		for (String category : categories) {
			JCheckBox chb = new JCheckBox(category);
			chb.setOpaque(false);
			chb.addActionListener(comp);
			chb.setSelected(true);
			list.add(chb);
		}
		return list;
	}

	/**
	 * get a list of JCheckBox for Persons
	 *
	 * @param mainFrame
	 * @param cbl
	 * @param comp
	 * @return
	 */
	public static List<JCheckBox> personsCreateCbList(MainFrame mainFrame,
	   List<JCheckBox> cbl, ActionListener comp) {
		List<JCheckBox> list = new ArrayList<>();
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		PersonDAO dao = new PersonDAO(session);
		for (JCheckBox cb : cbl) {
			if (cb.isSelected()) {
				Category category = (Category) cb.getClientProperty("CbCategory");
				List<Person> persons = dao.findByCategory(category);
				for (Person person : persons) {
					JCheckBox cbPerson = new JCheckBox(person.getFullNameAbbr());
					cbPerson.setOpaque(false);
					cbPerson.putClientProperty("CbPerson", person);
					cbPerson.addActionListener(comp);
					list.add(cbPerson);
				}
			}
		}
		model.commit();
		return list;
	}

	/**
	 * abandon changes for the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 */
	public static void abandonEntityChanges(MainFrame mainFrame, AbstractEntity entity) {
		try {
			if (entity.isTransient()) {
				return;
			}
			Model model = mainFrame.getBookModel();
			Session session = model.getSession();
			if (session != null && session.isOpen()) {
				Transaction transaction = session.beginTransaction();
				session.refresh(entity);
				transaction.commit();
			}
		} catch (HibernateException e) {
			LOG.err("EntityUtil.abandonEntityChanges(...)", e);
		}
	}

	/**
	 * get the tool tip for the given entity
	 *
	 * @param entity
	 * @return
	 */
	public static String tooltipGet(AbstractEntity entity) {
		return tooltipGet(entity, null, false);
	}

	/**
	 * get the tool tip for the given Entity
	 *
	 * @param entity
	 * @param date
	 * @param withDay
	 * @return
	 */
	public static String tooltipGet(AbstractEntity entity, Date date, boolean withDay) {
		StringBuilder buf = new StringBuilder();
		buf.append(Html.HTML_B);
		buf.append(Html.HEAD_B)
		   .append(Html.STYLE_B).append(Html.FONT_SIZE_DEFAULT).append(Html.STYLE_E)
		   .append(Html.HEAD_E);
		buf.append(Html.BODY_B);
		if (date != null) {
			buf.append(Html.intoP(
			   Html.intoB(
				  Html.intoI(DateUtil.timeToString(date, false)))));
		}
		if (entity.getName().trim().isEmpty()) {
			buf.append(Html.intoH(2, I18N.getMsg(entity.getObjType().toString())));
		} else {
			buf.append("<table width=\"300\">");
			buf.append(Html.TR_B).append(Html.TD_B);
			buf.append(Html.intoB(I18N.getColonMsg(entity.getObjType().toString())))
			   .append(" ")
			   .append(entity.getName());
			buf.append(Html.BR);
			switch (Book.getTYPE(entity)) {
				case LOCATION:
					toolTipLocationGet(buf, (Location) entity);
					break;
				case PERSON:
					toolTipPersonGet(buf, (Person) entity, date);
					break;
				case PLOT:
					toolTipPlotGet(buf, (Plot) entity);
					break;
				case RELATION:
					toolTipRelationGet(buf, (Relationship) entity);
					break;
				case SCENE:
					toolTipSceneGet(buf, (Scene) entity, withDay);
					break;
				case ITEM:
				case TAG:
					toolTipTagGet(buf, (AbstractTag) entity);
					break;
				case STRAND:
					toolTipStrandGet(buf, (Strand) entity);
					break;
				default:
					break;
			}
			buf.append(Html.TD_E)
			   .append(Html.TR_E)
			   .append(Html.TABLE_E);
		}
		buf.append(Html.BODY_E);
		buf.append(Html.HTML_E);
		return buf.toString();
	}

	/**
	 * get the tool tip of the given Location
	 *
	 * @param buf
	 * @param location
	 */
	private static void toolTipLocationGet(StringBuilder buf, Location location) {
		if (location == null) {
			return;
		}
		if (!location.getCity().isEmpty()) {
			buf.append(I18N.getColonMsg("location.city"));
			buf.append(" ");
			buf.append(location.getCity());
			buf.append(Html.BR);
		}
		if (!location.getCity().isEmpty()) {
			buf.append(I18N.getColonMsg("location.country"));
			buf.append(" ");
			buf.append(location.getCountry());
		}
		buf.append(Html.intoP(TextUtil.ellipsize(location.getDescription()), "\"margin-top:5px\""));
	}

	/**
	 * get the tool tip for the given Person
	 *
	 * @param buf
	 * @param person
	 * @param date
	 */
	private static void toolTipPersonGet(StringBuilder buf, Person person, Date date) {
		if (person == null) {
			return;
		}
		if (date != null && person.getBirthday() != null) {
			buf.append(I18N.getColonMsg("person.age"));
			buf.append(" ");
			buf.append(person.calculateAge(date));
			if (person.isDead(date)) {
				buf.append("+");
			}
			buf.append(Html.BR);
		}
		if (person.getGender() != null) {
			buf.append(I18N.getColonMsg("manage.persons.gender"));
			buf.append(" ");
			buf.append(person.getGender());
			buf.append(Html.BR);
		}
		if (person.getCategory() != null) {
			buf.append(I18N.getColonMsg("manage.persons.category"));
			buf.append(" ");
			buf.append(person.getCategory());
			buf.append(Html.BR);
		}
		if (person.getDescription() != null && !person.getDescription().isEmpty()) {
			buf.append(Html.intoP(TextUtil.ellipsize(person.getDescription()), "'margin-top:5px'"));
		}
	}

	/**
	 * get the tool tip for the given Plot
	 *
	 * @param buf
	 * @param plot
	 */
	private static void toolTipPlotGet(StringBuilder buf, Plot plot) {
		if (plot == null) {
			return;
		}
		if (plot.getCategory() != null && !plot.getCategory().isEmpty()) {
			buf.append(Html.intoB(I18N.getMsg("category"))).append(": ")
			   .append(plot.getCategory()).append(Html.BR);
		}
		if (plot.getDescription() != null && !plot.getDescription().isEmpty()) {
			buf.append(plot.getDescription());
		}
	}

	/**
	 * get the tool tip for the given Relationship
	 *
	 * @param buf
	 * @param relation
	 */
	private static void toolTipRelationGet(StringBuilder buf, Relationship relation) {
		List<Person> persons = relation.getPersons();
		if (persons != null && !persons.isEmpty()) {
			buf.append(Html.P_B);
			buf.append(Html.intoB(Html.intoI(I18N.getColonMsg("persons")))).append(" ");
			for (Person p : persons) {
				buf.append(p.getAbbr()).append(" ");
			}
			buf.append(Html.P_E);
		}
		List<Item> items = relation.getItems();
		if (items != null && !items.isEmpty()) {
			buf.append(Html.P_B);
			buf.append(Html.intoB(Html.intoI(I18N.getColonMsg("items")))).append(" ");
			for (Item p : items) {
				buf.append(p.getName()).append(" ");
			}
			buf.append(Html.P_E);
		}
		List<Location> locations = relation.getLocations();
		if (locations != null && !locations.isEmpty()) {
			buf.append(Html.P_B);
			buf.append(Html.intoB(Html.intoI(I18N.getColonMsg("locations")))).append(" ");
			for (Location p : locations) {
				buf.append(p.getName()).append(" ");
			}
			buf.append(Html.P_E);
		}
	}

	/**
	 * get the tools tip for the given Scene
	 *
	 * @param buf
	 * @param scene
	 * @param withDay
	 */
	private static void toolTipSceneGet(StringBuilder buf, Scene scene, boolean withDay) {
		buf.append(Html.P_B).append(Html.intoB(I18N.getColonMsg("chapter") + " "));
		buf.append((scene.getChapter() == null ? "" : scene.getChapter().getName()));
		buf.append(Html.P_E);
		if (scene.getSummary() != null && !scene.getSummary().isEmpty()) {
			buf.append(scene.getSummary(80));
		}
	}

	/**
	 * get the tools tip for the given Strand (not used)
	 *
	 * @param buf
	 * @param strand
	 */
	private static void toolTipStrandGet(StringBuilder buf, Strand strand) {
		//buf.append(strand.getName());
	}

	/**
	 * get the tools tip for the given Item/Tag
	 *
	 * @param buf
	 * @param tag
	 */
	private static void toolTipTagGet(StringBuilder buf, AbstractTag tag) {
		if (tag.getDescription() != null && !tag.getDescription().isEmpty()) {
			buf.append(TextUtil.ellipsize(tag.getDescription()));
		}
	}

	/**
	 * get info for the given Entity to delete, if no warning then delete
	 *
	 * @param mainFrame
	 * @param entity
	 * @return
	 */
	public static String getDeleteInfo(MainFrame mainFrame, AbstractEntity entity) {
		StringBuilder buf = new StringBuilder();
		boolean warnings = addDeletionInfo(mainFrame, entity, buf);
		if (warnings) {
			buf.append(Html.getHr());
		}
		Model model = mainFrame.getBookModel();
		Session session = model.getSession();
		session.beginTransaction();
		session.refresh(entity);
		buf.append(entity.toDetail(0));
		model.commit();
		return buf.toString();
	}

	/**
	 * collect delete info for the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @param buf
	 * @return
	 */
	private static boolean addDeletionInfo(MainFrame mainFrame,
	   AbstractEntity entity, StringBuilder buf) {
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		boolean warnings = false;
		switch (entity.getObjType()) {
			case CATEGORY: {
				Category category = (Category) entity;
				List<Person> persons = new CategoryDAO(session).findPersons(category);
				if (!persons.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("category.delete.warning")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("persons"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Person person : persons) {
						buf.append(Html.intoLI(person.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case CHAPTER: {
				Chapter chapter = (Chapter) entity;
				List<Scene> scenes = new ChapterDAO(session).findScenes(chapter);
				if (!scenes.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N
					   .getMsg("warning.chapter.has.assigned.scenes")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("scenes"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Scene scene : scenes) {
						buf.append(Html.intoLI(scene.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case GENDER: {
				Gender gender = (Gender) entity;
				List<Person> persons = new GenderDAO(session).findPersons(gender);
				if (!persons.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("gender.delete.warning")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("persons"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Person person : persons) {
						buf.append(Html.intoLI(person.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case ITEM: {
				Item item = (Item) entity;
				List<Itemlink> links = new ItemlinkDAO(session).findByItem(item);
				if (!links.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("itemlink.warning.delete.all")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("itemlink"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Itemlink link : links) {
						buf.append(Html.intoLI(link.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case PART: {
				Part part = (Part) entity;
				List<Chapter> chapters = new PartDAO(session).findChapters(part);
				if (!chapters.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("manage.parts.delete.warning")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("chapters"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Chapter chapter : chapters) {
						buf.append(Html.intoLI(chapter.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case SCENE: {
				Scene scene = (Scene) entity;
				List<Scene> scenes = new SceneDAO(session).findScenesWithRelativeSceneId(scene);
				if (!scenes.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("scene.relativedate.delete.warning")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("scenes"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Scene scene2 : scenes) {
						buf.append(Html.intoLI(scene2.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case STRAND: {
				Strand strand = (Strand) entity;
				List<Episode> episodes = new EpisodeDAO(session).findByStrand(strand);
				if (!episodes.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("manage.episodes.delete.warning")));
					buf.append(Html.P_E);
					warnings = true;
				}
				List<Scene> scenes = new StrandDAO(session).findScenes(strand);
				if (!scenes.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("manage.strands.delete.warning")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("scenes"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Scene scene : scenes) {
						buf.append(Html.intoLI(scene.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			case TAG: {
				Tag tag = (Tag) entity;
				List<Taglink> links = new TaglinkDAO(session).findByTag(tag);
				if (!links.isEmpty()) {
					buf.append(HTML_P_START);
					buf.append(Html.getWarning(I18N.getMsg("taglink.warning.delete.all")));
					buf.append(Html.P_E);
					buf.append(Html.BR);
					buf.append(I18N.getColonMsg("taglink"));
					buf.append(Html.BR);
					buf.append(Html.UL_B);
					for (Taglink link : links) {
						buf.append(Html.intoLI(link.toString()));
					}
					buf.append(Html.UL_E);
					warnings = true;
				}
				break;
			}
			default:
				break;
		}
		model.commit();
		return warnings;
	}

	/**
	 * get an entity for the given ID in given class
	 *
	 * @param mainFrame
	 * @param c
	 * @param entityId
	 * @return
	 */
	public static AbstractEntity get(MainFrame mainFrame,
	   Class<? extends AbstractEntity> c, Long entityId) {
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		AbstractEntity entity = (AbstractEntity) session.get(c, entityId);
		model.commit();
		refresh(mainFrame, entity);
		return entity;
	}

	/**
	 * refresh the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 */
	public static void refresh(MainFrame mainFrame, AbstractEntity entity) {
		if (entity == null) {
			return;
		}
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		session.refresh(entity);
		model.commit();
	}

	/**
	 * fill a JComboBox for the given Entity type
	 *
	 * @param mainFrame
	 * @param cb
	 * @param type
	 * @param toSel
	 * @param isNew
	 * @param addEmptyItem
	 */
	@SuppressWarnings("unchecked")
	public static void fillCB(MainFrame mainFrame, JComboBox cb,
	   Book.TYPE type, AbstractEntity toSel,
	   boolean isNew, boolean addEmptyItem) {
		cb.removeAllItems();
		cb.setRenderer(new LCREntity());
		if (addEmptyItem) {
			cb.addItem("");
		}
		List<AbstractEntity> entities = EntityUtil.findEntities(mainFrame, type);
		entities.forEach(entity -> cb.addItem(entity));
		if (toSel != null) {
			cb.setSelectedItem(toSel);
		}
	}

	/**
	 * get the simple name of the class for the given Entity
	 *
	 * @param entity
	 * @return
	 */
	public static String getClassName(AbstractEntity entity) {
		if (entity == null) {
			return "null";
		}
		return entity.getClass().getSimpleName().toLowerCase();
	}

	/**
	 * find an ENtity by the ID
	 *
	 * @param mainFrame
	 * @param type
	 * @param id
	 * @return
	 */
	public static AbstractEntity findEntityById(MainFrame mainFrame, Book.TYPE type, Long id) {
		List list = findEntities(mainFrame, type);
		if (list != null && !list.isEmpty()) {
			for (Object obj : list) {
				AbstractEntity entity = (AbstractEntity) obj;
				if (entity.getId().equals(id)) {
					return entity;
				}
			}
		}
		return null;
	}

	/**
	 * find all entities of the given TYPE
	 *
	 * @param mainFrame
	 * @param type
	 * @return
	 */
	public static List findEntities(MainFrame mainFrame, Book.TYPE type) {
		return findEntities(mainFrame, type.toString());
	}

	/**
	 * find all entities of the given type name
	 *
	 * @param mainFrame
	 * @param type
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static List findEntities(MainFrame mainFrame, String type) {
		//LOG.trace(TT + ".findEntities(mainFrame, key=" + key + ")");
		Model model = mainFrame.getBookModel();
		Session session = model.beginTransaction();
		List list = new ArrayList();
		switch (Book.getTYPE(type)) {
			case ATTRIBUTE:
			case ATTRIBUTES:
				list = new AttributeDAO(session).findAll();
				break;
			case CATEGORY:
			case CATEGORIES:
				list = new CategoryDAO(session).findAll();
				break;
			case CHAPTER:
			case CHAPTERS:
				list = new ChapterDAO(session).findAll();
				break;
			case EPISODE:
			case EPISODES:
				list = new EpisodeDAO(session).findAll();
				break;
			case ENDNOTE:
			case ENDNOTES:
				list = new EndnoteDAO(session).findAll();
				break;
			case EVENT:
			case EVENTS:
				list = new EventDAO(session).findAll();
				break;
			case GENDER:
			case GENDERS:
				list = new GenderDAO(session).findAll();
				break;
			case IDEA:
			case IDEAS:
				list = new IdeaDAO(session).findAll();
				break;
			case INTERNAL:
			case INTERNALS:
				list = new InternalDAO(session).findAll();
				break;
			case ITEM:
			case ITEMS:
				list = new ItemDAO(session).findAll();
				break;
			case ITEMLINK:
			case ITEMLINKS:
				list = new ItemlinkDAO(session).findAll();
				break;
			case LOCATION:
			case LOCATIONS:
				list = new LocationDAO(session).findAll();
				break;
			case MEMO:
			case MEMOS:
				list = new MemoDAO(session).findAll();
				break;
			case PART:
			case PARTS:
				list = new PartDAO(session).findAll();
				break;
			case PERSON:
			case PERSONS:
				list = new PersonDAO(session).findAll();
				break;
			case PLOT:
			case PLOTS:
				list = new PlotDAO(session).findAll();
				break;
			case RELATION:
			case RELATIONS:
				list = new RelationDAO(session).findAll();
				break;
			case SCENE:
			case SCENES:
				list = new SceneDAO(session).findAll();
				break;
			case STRAND:
			case STRANDS:
				list = new StrandDAO(session).findAll();
				break;
			case TAG:
			case TAGS:
				list = new TagDAO(session).findAll();
				break;
			case TAGLINK:
			case TAGLINKS:
				list = new TaglinkDAO(session).findAll();
				break;
			default:
				if (!type.equals("challenge")) {
					LOG.log("** " + TT + ".findEntities unable to find type='" + type + "'");
				}
		}
		model.commit();
		return list;
	}

	/**
	 * find all entities of the given type with the given name
	 *
	 * @param mainFrame
	 * @param type
	 * @param name
	 * @return
	 */
	public static AbstractEntity findEntityByName(MainFrame mainFrame, Book.TYPE type, String name) {
		List list = findEntities(mainFrame, type);
		if (list != null) {
			for (Object entity : list) {
				if (((AbstractEntity) entity).getName().equals(name)) {
					return ((AbstractEntity) entity);
				}
			}
		}
		return null;
	}

	/**
	 * get the list of ID as a String for the given Entities
	 *
	 * @param entities
	 * @return
	 */
	public static String getIds(List<?> entities) {
		if (entities == null) {
			return ("");
		}
		List<String> list = new ArrayList<>();
		for (Object a : entities) {
			list.add(((AbstractEntity) a).getId().toString());
		}
		return (ListUtil.join(list, ","));
	}

	/**
	 * get next Entity for the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @return
	 */
	public static AbstractEntity getEntityNext(MainFrame mainFrame, AbstractEntity entity) {
		List entities = EntityUtil.findEntities(mainFrame, entity.getObjType());
		if (entities != null) {
			for (int i = 0; i < entities.size(); i++) {
				AbstractEntity e = (AbstractEntity) entities.get(i);
				if (entity.getId().equals(e.getId())) {
					if (i > entities.size() - 2) {
						return (null);
					}
					return ((AbstractEntity) entities.get(i + 1));
				}
			}
		}
		return (null);
	}

	/**
	 * get the prior Entity for the given Entity
	 *
	 * @param mainFrame
	 * @param entity
	 * @return
	 */
	public static AbstractEntity getEntityPrior(MainFrame mainFrame, AbstractEntity entity) {
		if (entity == null) {
			return (null);
		}
		List entities = EntityUtil.findEntities(mainFrame, entity.getObjType());
		if (entities != null) {
			for (int i = 0; i < entities.size(); i++) {
				AbstractEntity e = (AbstractEntity) entities.get(i);
				if (entity.getId().equals(e.getId())) {
					if (i < 1) {
						return (null);
					}
					return ((AbstractEntity) entities.get(i - 1));
				}
			}
		}
		return (null);
	}

	/**
	 * create a new Entity from the given Entity template
	 *
	 * @param entity
	 * @return
	 */
	public static AbstractEntity createNewEntity(AbstractEntity entity) {
		return (entity == null ? null : createNewEntity(entity.getObjType()));
	}

	/**
	 * create a new Entity for the given TYPE
	 *
	 * @param type
	 * @return
	 */
	public static AbstractEntity createNewEntity(Book.TYPE type) {
		AbstractEntity n = null;
		switch (type) {
			case ATTRIBUTE:
				n = new Attribute();
				break;
			case CATEGORY:
				n = new Category();
				break;
			case CHAPTER:
				n = new Chapter();
				break;
			case ENDNOTE:
				n = new Endnote();
				break;
			case EVENT:
				n = new Event();
				break;
			case GENDER:
				n = new Gender();
				break;
			case IDEA:
				n = new Idea();
				break;
			case ITEM:
				n = new Item();
				break;
			case ITEMLINK:
				n = new Itemlink();
				break;
			case LOCATION:
				n = new Location();
				break;
			case MEMO:
				n = new Memo();
				break;
			case PART:
				n = new Part();
				break;
			case PERSON:
				n = new Person();
				break;
			case PLOT:
				n = new Plot();
				break;
			case RELATION:
				n = new Relationship();
				break;
			case SCENE:
				n = new Scene();
				break;
			case STRAND:
				n = new Strand();
				break;
			case STATUS:
				n = new Status();
				break;
			case TAG:
				n = new Tag();
				break;
			case TAGLINK:
				n = new Taglink();
				break;
			default:
				break;
		}
		return n;
	}

	/**
	 * get a String containing the list of names of the given Entities
	 *
	 * @param entities
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public static String getNames(List<?> entities) {
		//LOG.trace(TT+".getListName(entities nb="+(entities==null?"null":entities.size())+")");
		if (entities == null || entities.isEmpty()) {
			return ("");
		}
		List<String> list = new ArrayList<>();
		for (AbstractEntity e : (List<AbstractEntity>) entities) {
			list.add(e.getName());
		}
		return ListUtil.join(list);
	}

}
