// Copyright © 2016-2020 Andy Goryachev <[email protected]>
package goryachev.fx;
import goryachev.common.log.Log;
import goryachev.common.util.CKit;
import goryachev.common.util.Parsers;
import java.util.List;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;


/**
 * Vertically arranged Pane that lays out its child nodes using the following constraints:
 * PREF, FILL, percentage, or exact pixels.
 */
public class VPane
	extends Pane
{
	public static final double FILL = -1.0;
	public static final double PREF = -2.0;
	protected int gap;
	protected static final Object KEY_CONSTRAINT = new Object();
	
	
	public VPane(int hgap)
	{
		this.gap = hgap;
	}
	
	
	public VPane()
	{
	}
	
	
	public void setGap(int gap)
	{
		this.gap = gap;
	}
	
	
	/** adds a node with preferred height constraint */
	public void add(Node n)
	{
		massage(n);
		getChildren().add(n);
	}
	
	
	/** adds a node at the specified position */
	public void add(int ix, Node n)
	{
		massage(n);
		getChildren().add(ix, n);
	}
	
	
	/** adds a node with the specified height constraint */
	public void add(Node n, double constraint)
	{
		massage(n);
		getChildren().add(n);
		FX.setProperty(n, KEY_CONSTRAINT, Double.valueOf(constraint));
	}
	
	
	/** adds an empty region with the FILL constraint */
	public void fill()
	{
		Region n = new Region();
		massage(n);
		getChildren().add(n);
		FX.setProperty(n, KEY_CONSTRAINT, FILL);
	}
	
	
	/** adds a node with the FILL constraint */
	public void fill(Node n)
	{
		massage(n);
		getChildren().add(n);
		FX.setProperty(n, KEY_CONSTRAINT, FILL);
	}
	
	
	/** adds a node with the FILL constraint at the specified position */
	public void fill(int ix, Node n)
	{
		massage(n);
		getChildren().add(ix, n);
		FX.setProperty(n, KEY_CONSTRAINT, FILL);
	}
	
	
	protected void massage(Node n)
	{
		// once in an VPane, surrender your limitations!
		if(n instanceof Region)
		{
			Region r = (Region)n;
			r.setMaxHeight(Double.MAX_VALUE);
			r.setMaxWidth(Double.MAX_VALUE);
		}
	}

	
	protected double computePrefWidth(double height)
	{
		return h().computeWidth(height, true);	
	}
	

	protected double computeMinWidth(double height)
	{
		return h().computeWidth(height, false);
	}
	
	
	protected double computePrefHeight(double width)
	{
		return h().computeSizes(true);
	}

	
	protected double computeMinHeight(double width)
	{
		return h().computeSizes(false);
	}
	
	
	protected void layoutChildren()
	{
		try
		{
			h().layout();
		}
		catch(Exception e)
		{
			Log.err(e);
		}
	}
	
	
	protected Helper h()
	{
		return new Helper(getManagedChildren(), getInsets());
	}
	
	
	protected void setBounds(Node nd, double left, double top, double width, double height)
	{
		layoutInArea(nd, left, top, width, height, 0, HPos.CENTER, VPos.CENTER);
	}
	
	
	/** a shortcut to set padding on the panel */
	public void setPadding(double gap)
	{
		setPadding(new CInsets(gap));
	}
	
	
	/** a shortcut to set padding on the panel */
	public void setPadding(double ver, double hor)
	{
		setPadding(new CInsets(ver, hor));
	}
	
	
	/** a shortcut to set padding on the panel */
	public void setPadding(double top, double right, double bottom, double left)
	{
		setPadding(new CInsets(top, right, bottom, left));
	}


	public void remove(Node n)
	{
		getChildren().remove(n);
	}
	
	
	//
	
	
	public class Helper
	{
		public final List<Node> nodes;
		public final int sz;
		public final int gaps;
		public int top;
		public int bottom;
		public int left;
		public int right;
		public int[] size;
		public int[] pos;


		public Helper(List<Node> nodes, Insets m)
		{
			this.nodes = nodes;
			this.sz = nodes.size();
			top = CKit.round(m.getTop());
			bottom = CKit.round(m.getBottom());
			left = CKit.round(m.getLeft());
			right = CKit.round(m.getRight());
			gaps = (sz < 2) ? 0 : (gap * (sz - 1));
		}
		
		
		protected double getConstraint(Node n)
		{
			Object x = FX.getProperty(n, KEY_CONSTRAINT);
			return Parsers.parseDouble(x, PREF);
		}

		
		protected boolean isFixed(double x)
		{
			return (x > 1.0);
		}
		
		
		protected boolean isPercent(double x)
		{
			return (x < 1.0) && (x >= 0.0);
		}
		
		
		protected boolean isFill(double x)
		{
			return (x == FILL);
		}
		
		
		protected int computeSizes(boolean preferred)
		{
			int total = 0;
			
			for(int i=0; i<sz; i++)
			{
				Node n = nodes.get(i);
				double cc = getConstraint(n);
				int d;
				if(isFixed(cc))
				{
					d = CKit.ceil(cc);
				}
				else
				{
					if(preferred)
					{
						d = CKit.ceil(Math.max(n.prefHeight(-1), n.minHeight(-1)));
					}
					else
					{
						d = CKit.ceil(n.minHeight(-1));
					}
				}
				
				if(size != null)
				{
					size[i] = d;
				}
				
				total += d;
			}
			
			return total + top + bottom + gaps;
		}
		
		
		protected double computeWidth(double height, boolean preferred)
		{
			int max = 0;
			for(int i=0; i<sz; i++)
			{
				Node n = nodes.get(i);
				int d;
				if(preferred)
				{
					d = CKit.ceil(n.prefWidth(height));
				}
				else
				{
					d = CKit.ceil(n.minWidth(height));				
				}
				if(d > max)
				{
					max = d;
				}
			}
			
			return max + left + right;
		}
		
		
		protected void computePositions()
		{
			int start = left;
			pos = new int[sz + 1];
			pos[0] = start;
			
			for(int i=0; i<sz; i++)
			{
				start += (size[i] + gap);
				pos[i+1] = start;
			}
		}
		
		protected void adjust(int delta)
		{
			// space available for FILL/PERCENT columns
			int available = delta;
			// ratio of columns with percentage explicitly set
			double percent = 0;
			// number of FILL columns
			int fillsCount = 0;
			
			for(int i=0; i<sz; i++)
			{
				Node n = nodes.get(i);
				double cc = getConstraint(n);
				if(isPercent(cc))
				{
					// percent
					percent += cc;
					available += size[i];
				}
				else if(isFill(cc))
				{
					// fill
					fillsCount++;
					available += size[i];
				}
			}
			
			if(available < 0)
			{
				available = 0;
			}
			
			double percentFactor = (percent > 1.0) ? (1 / percent) : percent;
			int remaining = available;
			
			// PERCENT sizes first
			for(int i=0; i<sz; i++)
			{
				Node n = nodes.get(i);
				double cc = getConstraint(n);
				if(isPercent(cc))
				{
					double w;
					if(remaining > 0)
					{
						w = cc * available * percentFactor;
					}
					else
					{
						w = 0;
					}
					
					int d = CKit.round(w);
					size[i] = d;
					remaining -= d;
				}
			}
			
			// FILL sizes after PERCENT
			if(fillsCount > 0)
			{
				double cw = remaining / (double)fillsCount;
				
				for(int i=0; i<sz; i++)
				{
					Node n = nodes.get(i);
					double cc = getConstraint(n);
					if(isFill(cc))
					{
						double w;
						if(remaining >= 0)
						{
							w = Math.min(cw, remaining);
						}
						else
						{
							w = 0;
						}
						
						int d = CKit.ceil(w);
						size[i] = d;
						remaining -= d;
					}
				}
			}
		}
		

		public void applySizes()
		{
			computePositions();
			
			int w = CKit.floor(getWidth() - left - right);
			for(int i=0; i<sz; i++)
			{
				Node n = nodes.get(i);
				int y = pos[i];
				int h = size[i];
				
				setBounds(n, left, y, w, h);
			}
		}
		

		public void layout()
		{
			size = new int[sz];
			
			// populate size[] with preferred sizes
			int ph = computeSizes(true);
			int dh = CKit.floor(getHeight()) - ph;
			if(dh != 0)
			{
				adjust(dh);
			}

			applySizes();
		}
	}
}