/*
 * Copyright (C) 2004 NNL Technology AB
 * Visit www.infonode.net for information about InfoNode(R)
 * products and how to contact NNL Technology AB.
 *
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston,
 * MA 02111-1307, USA.
 */
// $Id: View.java,v 1.66 2005/12/03 14:34:34 jesper Exp $
package api.infonode.docking;

import java.awt.*;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.*;
import java.lang.ref.WeakReference;
import java.util.List;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import api.infonode.docking.drag.DockingWindowDragSource;
import api.infonode.docking.drop.InteriorDropInfo;
import api.infonode.docking.internal.ReadContext;
import api.infonode.docking.internal.ViewTitleBar;
import api.infonode.docking.internal.WriteContext;
import api.infonode.docking.internalutil.DropAction;
import api.infonode.docking.model.ViewItem;
import api.infonode.docking.model.ViewWriter;
import api.infonode.docking.properties.ViewProperties;
import api.infonode.docking.properties.ViewTitleBarProperties;
import api.infonode.gui.ComponentUtil;
import api.infonode.gui.panel.SimplePanel;
import api.infonode.properties.base.Property;
import api.infonode.properties.propertymap.PropertyMap;
import api.infonode.properties.propertymap.PropertyMapWeakListenerManager;
import api.infonode.properties.util.PropertyChangeListener;
import api.infonode.tabbedpanel.TabbedPanel;
import api.infonode.util.ChangeNotifyList;
import api.infonode.util.Direction;
import api.infonode.util.StreamUtil;

/**
 * <p>
 * A view is a docking window containing a component.
 * </p>
 *
 * <p>
 * A view can also contain a title bar that can be shown on either side of the view component. The
 * title bar is made visible by setting the visible property in the ViewTitleBarProperties in the
 * ViewProperties for this view. The title bar automatically inherits the view's title and icon but
 * it's possible to specify a specific title and icon for the title bar in the
 * ViewTitleBarProperties in the ViewProperties for this view.
 * </p>
 *
 * @author $Author: jesper $
 * @version $Revision: 1.66 $
 * @see api.infonode.docking.properties.ViewProperties
 * @see api.infonode.docking.properties.ViewTitleBarProperties
 */
public class View extends DockingWindow {

	private Component lastFocusedComponent;
	private HierarchyListener focusComponentListener = (HierarchyEvent e) -> {
		checkLastFocusedComponent();
	};
	private SimplePanel contentPanel = new SimplePanel();
	private ViewProperties rootProperties = new ViewProperties();
	private WeakReference lastRootWindow;
	private PropertyChangeListener listener = (Property property, Object valueContainer, Object oldValue, Object newValue) -> {
		fireTitleChanged();
	};

	private PropertyChangeListener titleBarPropertiesListener = (Property property, Object valueContainer, Object oldValue, Object newValue) -> {
		updateTitleBar(property, valueContainer);
	};

	private ViewTitleBar titleBar;
	private boolean isfocused = false;
	private List customTitleBarComponents;

	private WindowTab ghostTab;

	private DropAction titleBarDropAction = new DropAction() {
		@Override
		public boolean showTitle() {
			return false;
		}

		@Override
		public void execute(DockingWindow window, MouseEvent mouseEvent) {
			removeGhostTab();

			((AbstractTabWindow) getWindowParent()).addTab(window);
		}

		public void clear(DockingWindow window, DropAction newDropAction) {
			if (newDropAction != this) {
				removeGhostTab();
			}
		}

		private void removeGhostTab() {
			if (ghostTab != null) {
				TabbedPanel tp = ((AbstractTabWindow) getWindowParent()).getTabbedPanel();
				tp.removeTab(ghostTab);
				ghostTab = null;

				if (tp.getProperties().getEnsureSelectedTabVisible() && tp.getSelectedTab() != null) {
					tp.scrollTabToVisibleArea(tp.getSelectedTab());
				}
			}
		}

	};

	/**
	 * Constructor.
	 *
	 * @param title the title of the view
	 * @param icon the icon for the view
	 * @param component the component to place inside the view
	 */
	public View(String title, Icon icon, Component component) {
		super(new ViewItem());
		rootProperties.setTitle(title);
		rootProperties.setIcon(icon);
		getViewProperties().addSuperObject(rootProperties);
		super.setComponent(contentPanel);
		contentPanel.setComponent(component);

		PropertyMapWeakListenerManager.addWeakPropertyChangeListener(getViewProperties().getMap(),
				ViewProperties.TITLE,
				listener);
		PropertyMapWeakListenerManager.addWeakPropertyChangeListener(getViewProperties().getMap(),
				ViewProperties.ICON,
				listener);

		PropertyMapWeakListenerManager
				.addWeakPropertyChangeListener(getViewProperties().getViewTitleBarProperties().getMap(),
						ViewTitleBarProperties.VISIBLE,
						titleBarPropertiesListener);
		PropertyMapWeakListenerManager
				.addWeakPropertyChangeListener(getViewProperties().getViewTitleBarProperties().getMap(),
						ViewTitleBarProperties.CONTENT_TITLE_BAR_GAP,
						titleBarPropertiesListener);
		PropertyMapWeakListenerManager
				.addWeakPropertyChangeListener(getViewProperties().getViewTitleBarProperties().getMap(),
						ViewTitleBarProperties.ORIENTATION,
						titleBarPropertiesListener);

		updateTitleBar(null, null);

		init();
	}

	/**
	 * <p>
	 * Returns a list containing the custom window tab components. Changes to the list will be
	 * propagated to the tab.
	 * </p>
	 * <p>
	 * The custom tab components will be shown after the window title when the window tab is
	 * highlighted. The components are shown in the same order as they appear in the list. The
	 * custom tab components container layout is rotated with the tab direction.
	 * </p>
	 *
	 * @return a list containing the custom tab components, list elements are of type
	 * {@link JComponent}
	 * @since IDW 1.3.0
	 */
	public java.util.List getCustomTabComponents() {
		return getTab().getCustomTabComponentsList();
	}

	/**
	 * <p>
	 * Returns a list containing the custom view title bar components. Changes to the list will be
	 * propagated to the title bar.
	 * </p>
	 * <p>
	 * The custom title bar components will be shown after the view title in the title bar but
	 * before the close, minimize and restore buttons. The components are shown in the same order as
	 * they appear in the list. The custom title bar components container layout is rotated with the
	 * title bar direction.
	 * </p>
	 * <p>
	 * <strong>Note:</strong> The components are only shon if the title bar is visible, see
	 * {@link ViewTitleBarProperties}.
	 * </p>
	 *
	 * @return a list containing the custom title bar components, list elements are of type
	 * {@link JComponent}
	 * @since IDW 1.4.0
	 */
	public List getCustomTitleBarComponents() {
		if (customTitleBarComponents == null) {
			customTitleBarComponents = new ChangeNotifyList() {
				@Override
				protected void changed() {
					if (titleBar != null) {
						titleBar.updateCustomBarComponents(this);
					}
				}
			};
		}

		return customTitleBarComponents;
	}

	/**
	 * Gets the component inside the view.
	 *
	 * @return the component inside the view
	 * @since IDW 1.1.0
	 */
	public Component getComponent() {
		if (contentPanel.getComponentCount() < 1) {
			return null;
		}
		return contentPanel.getComponent(0);
	}

	/**
	 * Sets the component inside the view.
	 *
	 * @param component the component to place inside the view
	 * @since IDW 1.1.0
	 */
	@Override
	public void setComponent(Component component) {
		contentPanel.setComponent(component);
	}

	/**
	 * Returns the property values for this view.
	 *
	 * @return the property values for this view
	 */
	public ViewProperties getViewProperties() {
		return ((ViewItem) getWindowItem()).getViewProperties();
	}

	@Override
	protected void update() {
	}

	@Override
	public DockingWindow getChildWindow(int index) {
		return null;
	}

	@Override
	public int getChildWindowCount() {
		return 0;
	}

	void setLastFocusedComponent(Component component) {
		if (component != lastFocusedComponent) {
			if (lastFocusedComponent != null) {
				lastFocusedComponent.removeHierarchyListener(focusComponentListener);
			}

//      System.out.println("Focus: " + this + ", " + component.getClass() + " " + System.identityHashCode(component));
			lastFocusedComponent = component;

			if (lastFocusedComponent != null) {
				lastFocusedComponent.addHierarchyListener(focusComponentListener);
			}
		}
	}

	Component getFocusComponent() {
		checkLastFocusedComponent();
		return lastFocusedComponent;
	}

	@Override
	public boolean isFocusCycleRoot() {
		return true;
	}

	/**
	 * Restores focus to the last focused child component or, if no child component has had focus,
	 * the first focusable component inside the view.
	 *
	 * @since IDW 1.1.0
	 */
	@Override
	public void restoreFocus() {
		makeVisible();
		checkLastFocusedComponent();
		if (lastFocusedComponent == null) {
			ComponentUtil.smartRequestFocus(contentPanel);
		} else {
			lastFocusedComponent.requestFocusInWindow();
		}
	}

	@Override
	public Icon getIcon() {
		return getViewProperties().getIcon();
	}

	@Override
	protected void doReplace(DockingWindow oldWindow, DockingWindow newWindow) {
		throw new RuntimeException(View.class + ".replaceChildWindow called!");
	}

	@Override
	protected void doRemoveWindow(DockingWindow window) {
		throw new RuntimeException(View.class + ".removeChildWindow called!");
	}

	protected void write(ObjectOutputStream out, WriteContext context) throws IOException {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(baos);

		try {
			context.getViewSerializer().writeView(this, oos);
			getWindowItem().writeSettings(oos, context);
		} finally {
			oos.close();
		}

		out.writeInt(baos.size());
		baos.writeTo(out);
	}

	static View read(ObjectInputStream in, ReadContext context) throws IOException {
		int size = in.readInt();
		byte[] viewData = new byte[size];
		StreamUtil.readAll(in, viewData);
		ObjectInputStream viewIn = new ObjectInputStream(new ByteArrayInputStream(viewData));
		View view = context.getViewSerializer().readView(viewIn);

		if (view != null) {
			view.getWindowItem().readSettings(viewIn, context);
		}

		return view;
	}

	@Override
	protected DropAction doAcceptDrop(Point p, DockingWindow window) {
		if (getWindowParent() instanceof TabWindow && titleBar != null
				&& titleBar.contains(SwingUtilities.convertPoint(this, p, titleBar))) {
			return acceptInteriorDrop(p, window);
		}

		return getWindowParent() instanceof TabWindow && getWindowParent().getChildWindowCount() == 1
				? null
				: super.doAcceptDrop(p, window);
	}

	@Override
	protected DropAction acceptInteriorDrop(Point p, DockingWindow window) {
		if (getWindowParent() instanceof TabWindow && titleBar != null && window.getWindowParent() != getWindowParent()) {
			Point p2 = SwingUtilities.convertPoint(this, p, titleBar);
			if (titleBar.contains(p2)) {
				if (!getInteriorDropFilter().acceptDrop(new InteriorDropInfo(window, this, p))) {
					return null;
				}

				addGhostTab(window);
				Component c = getWindowParent() instanceof TabWindow ? getWindowParent() : this;
				getRootWindow().setDragRectangle(SwingUtilities.convertRectangle(c,
						new Rectangle(0,
								0,
								c.getWidth(),
								c.getHeight()),
						getRootWindow()));

				return titleBarDropAction;
			}
		}

		return null;
	}

	private void addGhostTab(DockingWindow window) {
		if (ghostTab == null) {
			ghostTab = ((AbstractTabWindow) getWindowParent()).createGhostTab(window);
			((AbstractTabWindow) getWindowParent()).getTabbedPanel().addTab(ghostTab);
			((AbstractTabWindow) getWindowParent()).getTabbedPanel().scrollTabToVisibleArea(ghostTab);
		}
	}

	@Override
	public String toString() {
		return getTitle();
	}

	@SuppressWarnings("unchecked")
	void setRootWindow(RootWindow newRoot) {
		if (newRoot == null) {
			return;
		}

		RootWindow last = lastRootWindow == null ? null : (RootWindow) lastRootWindow.get();

		if (last == newRoot) {
			return;
		}

		if (last != null) {
			last.removeView(this);
		}

		lastRootWindow = new WeakReference(newRoot);
		newRoot.addView(this);
	}

	@Override
	protected void setFocused(boolean focused) {
		//LOG.trace("View.setFocused(focused=" + LOG.trace(focused) + ") for " + rootProperties.getTitle());
		super.setFocused(focused);
		if (isfocused != focused) {
			isfocused = focused;
			if (focused) {
				getViewProperties().getViewTitleBarProperties().getNormalProperties()
						.addSuperObject(getViewProperties()
								.getViewTitleBarProperties().getFocusedProperties());
				contentPanel.getComponent(0).requestFocus();
			} else {
				getViewProperties().getViewTitleBarProperties().getNormalProperties()
						.removeSuperObject(getViewProperties()
								.getViewTitleBarProperties().getFocusedProperties());
			}
		}
	}

	protected void rootChanged(final RootWindow oldRoot, final RootWindow newRoot) {
		super.rootChanged(oldRoot, newRoot);
		setRootWindow(newRoot);

		if (oldRoot != getRootWindow()) {
			if (oldRoot != null) {
				rootProperties.removeSuperObject(oldRoot.getRootWindowProperties().getViewProperties());
			}

			if (getRootWindow() != null) {
				rootProperties.addSuperObject(getRootWindow().getRootWindowProperties().getViewProperties());
			}
		}
	}

	@Override
	protected PropertyMap getPropertyObject() {
		return getViewProperties().getMap();
	}

	@Override
	protected PropertyMap createPropertyObject() {
		return new ViewProperties().getMap();
	}

	@Override
	protected boolean needsTitleWindow() {
		return getViewProperties().getAlwaysShowTitle();
	}

	private void checkLastFocusedComponent() {
		if (lastFocusedComponent != null && !SwingUtilities.isDescendingFrom(lastFocusedComponent, this)) {
			lastFocusedComponent.removeHierarchyListener(focusComponentListener);
			lastFocusedComponent = null;
		}
	}

	@Override
	void removeWindowComponent(DockingWindow window) {
	}

	@Override
	void restoreWindowComponent(DockingWindow window) {
	}

	private void updateTitleBar(Property property, Object valueContainer) {
		boolean changed = valueContainer == null;

		ViewTitleBarProperties titleBarProperties = getViewProperties().getViewTitleBarProperties();
		//System.out.println("Updating title bar " + changed + "  " + property);

		if (changed || property == ViewTitleBarProperties.VISIBLE) {
			if (titleBarProperties.getVisible()) {
				if (titleBar == null) {
					titleBar = new ViewTitleBar(this);
					new DockingWindowDragSource(titleBar, (MouseEvent mouseEvent) -> getWindowProperties().getDragEnabled() ? startDrag(getRootWindow()) : null);
					titleBar.addMouseListener(new MouseAdapter() {
						@Override
						public void mousePressed(MouseEvent e) {
							fireTabWindowMouseButtonEvent(e);
							checkPopupMenu(e);
						}

						@Override
						public void mouseClicked(MouseEvent e) {
							fireTabWindowMouseButtonEvent(e);
						}

						@Override
						public void mouseReleased(MouseEvent e) {
							fireTabWindowMouseButtonEvent(e);
							checkPopupMenu(e);
						}

						private void checkPopupMenu(MouseEvent e) {
							if (e.isPopupTrigger()) {
								showPopupMenu(e);
							}
						}
					});

					if (customTitleBarComponents != null) {
						titleBar.updateCustomBarComponents(customTitleBarComponents);
					}
					changed = true;
				}
			} else if (titleBar != null) {
				remove(titleBar);
				titleBar.dispose();
				titleBar = null;

				changed = true;
			}
		}

		if (changed || property == ViewTitleBarProperties.ORIENTATION) {
			if (titleBar != null) {
				remove(titleBar);
				add(titleBar, ComponentUtil.getBorderLayoutOrientation(titleBarProperties.getOrientation()));

				changed = true;
			}
		}

		if (changed || property == ViewTitleBarProperties.CONTENT_TITLE_BAR_GAP) {
			if (titleBar != null) {
				Direction orientation = titleBarProperties.getOrientation();
				int contentBarGap = titleBarProperties.getContentTitleBarGap();
				contentPanel.setBorder(new EmptyBorder(orientation == Direction.UP ? contentBarGap : 0,
						orientation == Direction.LEFT ? contentBarGap : 0,
						orientation == Direction.DOWN
								? contentBarGap
								: 0,
						orientation == Direction.RIGHT ? contentBarGap : 0));
			} else {
				contentPanel.setBorder(null);
			}
		}
	}

	@Override
	protected void updateButtonVisibility() {
		super.updateButtonVisibility();

		if (titleBar != null) {
			titleBar.updateViewButtons(null);
		}
	}

	@Override
	protected void write(ObjectOutputStream out, WriteContext context, ViewWriter viewWriter) throws IOException {
		out.writeInt(WindowIds.VIEW);
		viewWriter.writeView(this, out, context);
	}
}
