/*
Copyright (c) 2022-2023 Andrew Auclair

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
 */
package io.github.andrewauclair.moderndocking.internal;

import io.github.andrewauclair.moderndocking.Dockable;
import io.github.andrewauclair.moderndocking.DockingRegion;
import io.github.andrewauclair.moderndocking.api.DockingAPI;
import io.github.andrewauclair.moderndocking.settings.Settings;
import java.awt.BorderLayout;
import java.awt.Window;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.List;
import javax.swing.JSplitPane;
import javax.swing.plaf.basic.BasicSplitPaneDivider;
import javax.swing.plaf.basic.BasicSplitPaneUI;

/**
 * DockingPanel that has a split pane with 2 dockables, split can be vertical or horizontal
 */
public class DockedSplitPanel extends DockingPanel implements MouseListener, PropertyChangeListener {
	/**
	 * Panel in the left/top part of the split
	 */
	private DockingPanel left = null;
	/**
	 * Panel in the right/bottom part of the split
	 */
	private DockingPanel right = null;

	/**
	 * The split pane we're controlling
	 */
	private final JSplitPane splitPane = new JSplitPane();
	/**
	 * The parent panel of this split panel
	 */
	private DockingPanel parent;
	/**
	 * The docking instance this panel belongs to
	 */
	private final DockingAPI docking;
	/**
	 * The window this panel is in
	 */
	private final Window window;
	/**
	 * The anchor this panel belongs to, if any
	 */
	private String anchor = "";

	/**
	 * the last divider proportion that setDividerLocation was called with
	 */
	private double lastRequestedDividerProportion;

	/**
	 * Create a new DockedSplitPanel
	 *
	 * @param docking The docking instance
	 * @param window The window this panel is in. Used to tell the child DockableWrappers what Window they are a part of
	 * @param anchor The anchor associated with this docking panel
	 */
	public DockedSplitPanel(DockingAPI docking, Window window, String anchor) {
		this.docking = docking;
		this.window = window;
		this.anchor = anchor;

		setLayout(new BorderLayout());

		splitPane.setContinuousLayout(true);
		splitPane.setResizeWeight(0.5);
		splitPane.setBorder(null);
		splitPane.addPropertyChangeListener(JSplitPane.DIVIDER_LOCATION_PROPERTY, this);

		setDividerLocation(splitPane.getResizeWeight());

		lastRequestedDividerProportion = splitPane.getResizeWeight();

		if (splitPane.getUI() instanceof BasicSplitPaneUI) {
			((BasicSplitPaneUI) splitPane.getUI()).getDivider().addMouseListener(this);
		}

		add(splitPane, BorderLayout.CENTER);
	}

	/**
	 * Retrieve the last double that we used to set the divider proportion with. This helps us properly
	 * restore divider locations when restoring from a layout.
	 *
	 * @return Requested divider proportion
	 */
	public double getLastRequestedDividerProportion() {
		return lastRequestedDividerProportion;
	}

	/**
	 * Set the divider location of the splitpane. Tricks are needed to properly set the position.
	 * The position cannot be set until the JSplitPane is displayed on screen.
	 *
	 * @param proportion The new proportion of the splitpane
	 */
	public void setDividerLocation(final double proportion) {
		lastRequestedDividerProportion = proportion;

		// calling setDividerLocation on a JSplitPane that isn't visible does nothing, so we need to check if it is showing first
		if (splitPane.isShowing()) {
			if (splitPane.getWidth() > 0 && splitPane.getHeight() > 0) {
				splitPane.setDividerLocation(proportion);
			}
			else {
				// split hasn't been completely calculated yet, wait until componentResize
				splitPane.addComponentListener(new ComponentAdapter() {
					@Override
					public void componentResized(ComponentEvent e) {
						// remove this listener, it's a one off
						splitPane.removeComponentListener(this);
						// call the function again, this time it should actually set the divider location
						setDividerLocation(proportion);
					}
				});
			}
		}
		else {
			// split hasn't been shown yet, wait until it's showing
			splitPane.addHierarchyListener(new HierarchyListener() {
				@Override
				public void hierarchyChanged(HierarchyEvent e) {
					boolean isShowingChangeEvent = (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0;

					if (isShowingChangeEvent && splitPane.isShowing()) {
						// remove this listener, it's a one off
						splitPane.removeHierarchyListener(this);
						// call the function again, this time it might set the size or wait for componentResize
						setDividerLocation(proportion);
					}
				}
			});
		}
	}

	/**
	 * Used to set the real divider location, which is set as an int, not a double.
	 *
	 * @param location The new proportion of the splitpane
	 */
	public void setDividerLocation(final int location) {
		if (splitPane.isShowing()) {
			if ((splitPane.getWidth() > 0) && (splitPane.getHeight() > 0)) {
				splitPane.setDividerLocation(location);

				docking.getAppState().persist();
			}
			else {
				// split hasn't been completely calculated yet, wait until componentResize
				splitPane.addComponentListener(new ComponentAdapter() {
					@Override
					public void componentResized(ComponentEvent e) {
						// remove this listener, it's a one off
						splitPane.removeComponentListener(this);
						// call the function again, this time it should actually set the divider location
						setDividerLocation(location);
					}
				});
			}
		}
		else {
			// split hasn't been shown yet, wait until it's showing
			splitPane.addHierarchyListener(new HierarchyListener() {
				@Override
				public void hierarchyChanged(HierarchyEvent e) {
					boolean isShowingChangeEvent = (e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0;

					if (isShowingChangeEvent && splitPane.isShowing()) {
						// remove this listener, it's a one off
						splitPane.removeHierarchyListener(this);
						// call the function again, this time it might set the size or wait for componentResize
						setDividerLocation(location);
					}
				}
			});
		}
	}

	/**
	 * Access to the underlying JSplitPane. This is required so that we can pull a bunch of values for saving layouts.
	 *
	 * @return The JSplitPane used by this DockedSplitPanel
	 */
	public JSplitPane getSplitPane() {
		return splitPane;
	}

	/**
	 * Get the left/top component of the split
	 *
	 * @return Left/top component in the split
	 */
	public DockingPanel getLeft() {
		return left;
	}

	/**
	 * Set the panel in the left/top of split
	 *
	 * @param panel New left/top panel
	 */
	public void setLeft(DockingPanel panel) {
		left = panel;
		left.setParent(this);
//		left.setAnchor(anchor);

		// remember where the divider was and put it back
		int dividerLocation = splitPane.getDividerLocation();

		splitPane.setLeftComponent(panel);

		splitPane.setDividerLocation(dividerLocation);
	}

	/**
	 * Get the right/bottom component of the split
	 *
	 * @return Right/bottom component in the split
	 */
	public DockingPanel getRight() {
		return right;
	}

	/**
	 * Set the right panel of the split
	 *
	 * @param panel New right panel
	 */
	public void setRight(DockingPanel panel) {
		right = panel;
		right.setParent(this);
//		right.setAnchor(anchor);

		// remember where the divider was and put it back
		int dividerLocation = splitPane.getDividerLocation();

		splitPane.setRightComponent(panel);

		splitPane.setDividerLocation(dividerLocation);
	}

	/**
	 * Set the orientation of the split pane
	 *
	 * @param orientation New orientation of the split pane
	 */
	public void setOrientation(int orientation) {
		splitPane.setOrientation(orientation);

		if (splitPane.getUI() instanceof BasicSplitPaneUI) {
			// grab the divider from the UI and remove the border from it
			BasicSplitPaneDivider divider = ((BasicSplitPaneUI) splitPane.getUI())
					.getDivider();

			if (divider != null && divider.getBorder() != null) {
				divider.setBorder(null);
			}
		}
	}

	@Override
	public String getAnchor() {
		return anchor;
	}

	@Override
	public void setAnchor(String anchor) {
		this.anchor = anchor;
	}

	@Override
	public void setParent(DockingPanel parent) {
		this.parent = parent;
	}

	@Override
	public void dock(Dockable dockable, DockingRegion region, double dividerProportion) {
		DockableWrapper wrapper = DockingInternal.get(docking).getWrapper(dockable);

		// docking to the center of a split isn't something we allow
		// wouldn't be difficult to support, but isn't a complication we want in this framework
		if (region == DockingRegion.CENTER) {
			region = splitPane.getOrientation() == JSplitPane.HORIZONTAL_SPLIT ? DockingRegion.WEST : DockingRegion.NORTH;
		}

		wrapper.setWindow(window);

		DockedSplitPanel split = new DockedSplitPanel(docking, window, anchor);
		parent.replaceChild(this, split);

		DockingPanel newPanel;

		if (Settings.alwaysDisplayTabsMode()) {
			newPanel = new DockedTabbedPanel(docking, wrapper, anchor);
		}
		else {
			newPanel = new DockedSimplePanel(docking, wrapper, anchor);
		}

		if (region == DockingRegion.EAST || region == DockingRegion.SOUTH) {
			split.setLeft(this);
			split.setRight(newPanel);
			dividerProportion = 1.0 - dividerProportion;
		}
		else {
			split.setLeft(newPanel);
			split.setRight(this);
		}

		if (region == DockingRegion.EAST || region == DockingRegion.WEST) {
			split.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
		}
		else {
			split.setOrientation(JSplitPane.VERTICAL_SPLIT);
		}

		split.setDividerLocation(dividerProportion);
	}

	@Override
	public void undock(Dockable dockable) {
	}

	@Override
	public void replaceChild(DockingPanel child, DockingPanel newChild) {
		if (left == child) {
			setLeft(newChild);
		}
		else if (right == child) {
			setRight(newChild);
		}
	}

	@Override
	public void removeChild(DockingPanel child) {
		// safety against partially configured layout restorations
		if (parent == null) {
			return;
		}

		if (left == child) {
			parent.replaceChild(this, right);
		}
		else if (right == child) {
			parent.replaceChild(this, left);
		}
	}

	public List<DockingPanel> getChildren() {
		return Arrays.asList(left, right);
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		if (e.getClickCount() >= 2) {
			setDividerLocation(splitPane.getResizeWeight());
		}
	}

	@Override
	public void mousePressed(MouseEvent e) {
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		docking.getAppState().persist();
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		docking.getAppState().persist();
	}
}
