/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.forms;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;

import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.forms.widgets.ScrolledPageBook;
/**
 * This managed form part handles the 'details' portion of the
 * 'master/details' block. It has a page book that manages pages
 * of details registered for the current selection.
 * <p>By default, details part accepts any number of pages.
 * If dynamic page provider is registered, this number may
 * be excessive. To avoid running out of steam (by creating
 * a large number of pages with widgets on each), maximum
 * number of pages can be set to some reasonable value (e.g. 10).
 * When this number is reached, old pages (those created first)
 * will be removed and disposed as new ones are added. If
 * the disposed pages are needed again after that, they
 * will be created again.
 *
 * @since 3.0
 */
public final class DetailsPart implements IFormPart, IPartSelectionListener {
	private IManagedForm managedForm;
	private ScrolledPageBook pageBook;
	private IFormPart masterPart;
	private IStructuredSelection currentSelection;
	private Hashtable<Object, PageBag> pages;
	private IDetailsPageProvider pageProvider;
	private int pageLimit=Integer.MAX_VALUE;

	private static class PageBag {
		private static int counter;
		private int ticket;
		private IDetailsPage page;
		private boolean fixed;

		public PageBag(IDetailsPage page, boolean fixed) {
			this.page= page;
			this.fixed = fixed;
			this.ticket = ++counter;
		}
		public int getTicket() {
			return ticket;
		}
		public IDetailsPage getPage() {
			return page;
		}
		public void dispose() {
			page.dispose();
			page=null;
		}
		public boolean isFixed() {
			return fixed;
		}
		public static int getCurrentTicket() {
			return counter;
		}
	}
/**
 * Creates a details part by wrapping the provided page book.
 * @param mform the parent form
 * @param pageBook the page book to wrap
 */
	public DetailsPart(IManagedForm mform, ScrolledPageBook pageBook) {
		this.pageBook = pageBook;
		pages = new Hashtable<>();
		initialize(mform);
	}
/**
 * Creates a new details part in the provided form by creating
 * the page book.
 * @param mform the parent form
 * @param parent the composite to create the page book in
 * @param style the style for the page book
 */
	public DetailsPart(IManagedForm mform, Composite parent, int style) {
		this(mform, mform.getToolkit().createPageBook(parent, style|SWT.V_SCROLL|SWT.H_SCROLL));
	}
/**
 * Registers the details page to be used for all the objects of
 * the provided object class.
 * @param objectClass an object of type 'java.lang.Class' to be used
 * as a key for the provided page
 * @param page the page to show for objects of the provided object class
 */
	public void registerPage(Object objectClass, IDetailsPage page) {
		registerPage(objectClass, page, true);
	}

	private void registerPage(Object objectClass, IDetailsPage page, boolean fixed) {
		pages.put(objectClass, new PageBag(page, fixed));
		page.initialize(managedForm);
	}
/**
 * Sets the dynamic page provider. The dynamic provider can return
 * different pages for objects of the same class based on their state.
 * @param provider the provider to use
 */
	public void setPageProvider(IDetailsPageProvider provider) {
		this.pageProvider = provider;
	}
/**
 * Commits the part by committing the current page.
 * @param onSave <code>true</code> if commit is requested as a result
 * of the 'save' action, <code>false</code> otherwise.
 */
	@Override
	public void commit(boolean onSave) {
		IDetailsPage page = getCurrentPage();
		if (page != null)
			page.commit(onSave);
	}
/**
 * Returns the current page visible in the part.
 * @return the current page
 */
	public IDetailsPage getCurrentPage() {
		Control control = pageBook.getCurrentPage();
		if (control != null) {
			Object data = control.getData();
			if (data instanceof IDetailsPage)
				return (IDetailsPage) data;
		}
		return null;
	}

	@Override
	public void dispose() {
		for (Enumeration<PageBag> enm = pages.elements(); enm.hasMoreElements();) {
			PageBag pageBag = enm.nextElement();
			pageBag.dispose();
		}
	}

	@Override
	public void initialize(IManagedForm form) {
		this.managedForm = form;
	}
/**
 * Tests if the currently visible page is dirty.
 * @return <code>true</code> if the page is dirty, <code>false</code> otherwise.
 */
	@Override
	public boolean isDirty() {
		IDetailsPage page = getCurrentPage();
		if (page != null)
			return page.isDirty();
		return false;
	}
/**
 * Tests if the currently visible page is stale and needs refreshing.
 * @return <code>true</code> if the page is stale, <code>false</code> otherwise.
 */
	@Override
	public boolean isStale() {
		IDetailsPage page = getCurrentPage();
		if (page != null)
			return page.isStale();
		return false;
	}

/**
 * Refreshes the current page.
 */
	@Override
	public void refresh() {
		IDetailsPage page = getCurrentPage();
		if (page != null)
			page.refresh();
	}
/**
 * Sets the focus to the currently visible page.
 */
	@Override
	public void setFocus() {
		IDetailsPage page = getCurrentPage();
		if (page != null)
			page.setFocus();
	}

	@Override
	public boolean setFormInput(Object input) {
		return false;
	}

	@Override
	public void selectionChanged(IFormPart part, ISelection selection) {
		this.masterPart = part;
		if (currentSelection != null) {
		}
		if (selection instanceof IStructuredSelection)
			currentSelection = (IStructuredSelection) selection;
		else
			currentSelection = null;
		update();
	}
	private void update() {
		Object key = null;
		if (currentSelection != null) {
			for (Iterator<?> iter = currentSelection.iterator(); iter.hasNext();) {
				Object obj = iter.next();
				if (key == null)
					key = getKey(obj);
				else if (getKey(obj).equals(key) == false) {
					key = null;
					break;
				}
			}
		}
		showPage(key);
	}
	private Object getKey(Object object) {
		if (pageProvider!=null) {
			Object key = pageProvider.getPageKey(object);
			if (key!=null)
				return key;
		}
		return object.getClass();
	}
	private void showPage(final Object key) {
		checkLimit();
		final IDetailsPage oldPage = getCurrentPage();
		if (key != null) {
			PageBag pageBag = pages.get(key);
			IDetailsPage page = pageBag!=null?pageBag.getPage():null;
			if (page==null) {
				// try to get the page dynamically from the provider
				if (pageProvider!=null) {
					page = pageProvider.getPage(key);
					if (page!=null) {
						registerPage(key, page, false);
					}
				}
			}
			if (page != null) {
				final IDetailsPage fpage = page;
				BusyIndicator.showWhile(pageBook.getDisplay(), () -> {
					if (!pageBook.hasPage(key)) {
						Composite parent = pageBook.createPage(key);
						fpage.createContents(parent);
						parent.setData(fpage);
					}
					//commit the current page
					if (oldPage!=null && oldPage.isDirty())
						oldPage.commit(false);
					//refresh the new page
					if (fpage.isStale())
						fpage.refresh();
					fpage.selectionChanged(masterPart, currentSelection);
					pageBook.showPage(key);
				});
				return;
			}
		}
		// If we are switching from an old page to nothing,
		// don't loose data
		if (oldPage!=null && oldPage.isDirty())
			oldPage.commit(false);
		pageBook.showEmptyPage();
	}
	private void checkLimit() {
		if (pages.size() <= getPageLimit()) return;
		// overflow
		int currentTicket = PageBag.getCurrentTicket();
		int cutoffTicket = currentTicket - getPageLimit();
		for (Enumeration<Object> enm=pages.keys(); enm.hasMoreElements();) {
			Object key = enm.nextElement();
			PageBag pageBag = pages.get(key);
			if (pageBag.getTicket()<=cutoffTicket) {
				// candidate - see if it is active and not fixed
				if (!pageBag.isFixed() && !pageBag.getPage().equals(getCurrentPage())) {
					// drop it
					pageBag.dispose();
					pages.remove(key);
					pageBook.removePage(key, false);
				}
			}
		}
	}
	/**
	 * Returns the maximum number of pages that should be
	 * maintained in this part. When an attempt is made to
	 * add more pages, old pages are removed and disposed
	 * based on the order of creation (the oldest pages
	 * are removed). The exception is made for the
	 * page that should otherwise be disposed but is
	 * currently active.
	 * @return maximum number of pages for this part
	 */
	public int getPageLimit() {
		return pageLimit;
	}
	/**
	 * Sets the page limit for this part.
	 * @see #getPageLimit()
	 * @param pageLimit the maximum number of pages that
	 * should be maintained in this part.
	 */
	public void setPageLimit(int pageLimit) {
		this.pageLimit = pageLimit;
		checkLimit();
	}
}
