/*******************************************************************************
 * Copyright (c) 2006, 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 (original file org.eclipse.ui.texteditor.templates.ColumnLayout)
 *     Tom Schindl <tom.schindl@bestsolution.at> - refactored to be widget independent (bug 171824)
 *                                               - fix for bug 178280, 184342, 184045, 208014, 214532
 *     Micah Hainline <micah_hainline@yahoo.com> - fix in bug: 208335
 *******************************************************************************/
package org.eclipse.jface.layout;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.ColumnLayoutData;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.ColumnWeightData;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Widget;

/**
 * The AbstractColumnLayout is a {@link Layout} used to set the size of a table
 * in a consistent way even during a resize unlike a {@link TableLayout} which
 * only sets initial sizes.
 *
 * <p>
 * <b>You can only add the layout to a container whose only child is the
 * table/tree control you want the layouts applied to.</b>
 * </p>
 *
 * @since 3.4
 */
public abstract class AbstractColumnLayout extends Layout {
	private static int COLUMN_TRIM;
	static {
		if (Util.isWindows()) {
			COLUMN_TRIM = 4;
		} else if (Util.isMac()) {
			COLUMN_TRIM = 24;
		} else {
			COLUMN_TRIM = 3;
		}
	}

	static final boolean IS_GTK = Util.isGtk();

	/**
	 * Key used to restore the layout data in the columns data-slot
	 * @since 3.5
	 */
	protected static final String LAYOUT_DATA = Policy.JFACE + ".LAYOUT_DATA"; //$NON-NLS-1$

	private boolean inupdateMode = false;

	private boolean relayout = true;

	private boolean adjustForScrollBar = false;

	private Listener resizeListener = event -> {
		if (!inupdateMode) {
			updateColumnData(event.widget);
		}
	};

	/**
	 * Creates a new abstract column layout.
	 */
	public AbstractColumnLayout() {
	}

	/**
	 * Creates a new abstract column layout.
	 *
	 * @param adjustForScrollBar
	 *            <code>true</code> if the layout should reserve space for the
	 *            vertical scroll bar
	 * @since 3.12
	 */
	public AbstractColumnLayout(boolean adjustForScrollBar) {
		this.adjustForScrollBar = adjustForScrollBar;
	}

	/**
	 * Adds a new column of data to this table layout.
	 *
	 * @param column
	 *            the column
	 *
	 * @param data
	 *            the column layout data
	 */
	public void setColumnData(Widget column, ColumnLayoutData data) {

		if (column.getData(LAYOUT_DATA) == null) {
			column.addListener(SWT.Resize, resizeListener);
		}

		column.setData(LAYOUT_DATA, data);
	}

	/**
	 * Compute the size of the table or tree based on the ColumnLayoutData and
	 * the width and height hint.
	 *
	 * @param scrollable
	 *            the widget to compute
	 * @param wHint
	 *            the width hint
	 * @param hHint
	 *            the height hint
	 * @return Point where x is the width and y is the height
	 */
	private Point computeTableTreeSize(Scrollable scrollable, int wHint,
			int hHint) {
		Point result = scrollable.computeSize(wHint, hHint);

		int width = scrollable.getBorderWidth() * 2;
		if (adjustForScrollBar && scrollable.getVerticalBar() != null) {
			width += scrollable.getVerticalBar().getSize().x;
		}
		int size = getColumnCount(scrollable);
		for (int i = 0; i < size; ++i) {
			ColumnLayoutData layoutData = getLayoutData(scrollable, i);
			if (layoutData instanceof ColumnPixelData) {
				ColumnPixelData col = (ColumnPixelData) layoutData;
				width += col.width;
				if (col.addTrim) {
					width += getColumnTrim();
				}
			} else if (layoutData instanceof ColumnWeightData) {
				ColumnWeightData col = (ColumnWeightData) layoutData;
				width += col.minimumWidth;
			} else {
				Assert.isTrue(false, "Unknown column layout data"); //$NON-NLS-1$
			}
		}
		result.x = width;

		return result;
	}

	/**
	 * Layout the scrollable based on the supplied width and area. Only increase
	 * the size of the scrollable if increase is <code>true</code>.
	 *
	 * @param scrollable
	 * @param width
	 * @param area
	 * @param increase
	 */
	private void layoutTableTree(final Scrollable scrollable, final int width,
			final Rectangle area, final boolean increase) {
		final int numberOfColumns = getColumnCount(scrollable);
		final int[] widths = new int[numberOfColumns];

		final int[] weightColumnIndices = new int[numberOfColumns];
		int numberOfWeightColumns = 0;

		int fixedWidth = 0;
		int totalWeight = 0;

		// First calc space occupied by fixed columns
		for (int i = 0; i < numberOfColumns; i++) {
			ColumnLayoutData col = getLayoutData(scrollable, i);
			if (col instanceof ColumnPixelData) {
				ColumnPixelData cpd = (ColumnPixelData) col;
				int pixels = cpd.width;
				if (cpd.addTrim) {
					pixels += getColumnTrim();
				}
				widths[i] = pixels;
				fixedWidth += pixels;
			} else if (col instanceof ColumnWeightData) {
				ColumnWeightData cw = (ColumnWeightData) col;
				weightColumnIndices[numberOfWeightColumns] = i;
				numberOfWeightColumns++;
				totalWeight += cw.weight;
			} else {
				Assert.isTrue(false, "Unknown column layout data"); //$NON-NLS-1$
			}
		}
		if (adjustForScrollBar) {
			ScrollBar verticalBar = scrollable.getVerticalBar();
			if (verticalBar != null && scrollable.getScrollbarsMode() == SWT.NONE && !verticalBar.isVisible()) {
				fixedWidth += verticalBar.getSize().x;
			}
		}

		boolean recalculate;
		do {
			recalculate = false;
			for (int i = 0; i < numberOfWeightColumns; i++) {
				int colIndex = weightColumnIndices[i];
				ColumnWeightData cw = (ColumnWeightData) getLayoutData(
						scrollable, colIndex);
				final int minWidth = cw.minimumWidth;
				final int allowedWidth = totalWeight == 0 ? 0
						: (width - fixedWidth) * cw.weight / totalWeight;
				if (allowedWidth < minWidth) {
					/*
					 * if the width assigned by weight is less than the minimum,
					 * then treat this column as fixed, remove it from weight
					 * calculations, and recalculate other weights.
					 */
					numberOfWeightColumns--;
					totalWeight -= cw.weight;
					fixedWidth += minWidth;
					widths[colIndex] = minWidth;
					System.arraycopy(weightColumnIndices, i + 1,
							weightColumnIndices, i, numberOfWeightColumns - i);
					recalculate = true;
					break;
				}
				widths[colIndex] = allowedWidth;
			}
		} while (recalculate);

		if (increase) {
			scrollable.setBounds(0, 0, area.width, area.height);
		}

		inupdateMode = true;
		setColumnWidths(scrollable, widths);
		scrollable.update();
		inupdateMode = false;

		if (!increase) {
			scrollable.setBounds(0, 0, area.width, area.height);
		}
	}

	@Override
	protected Point computeSize(Composite composite, int wHint, int hHint,
			boolean flushCache) {
		return computeTableTreeSize(getControl(composite), wHint, hHint);
	}

	@Override
	protected void layout(Composite composite, boolean flushCache) {
		Rectangle area = composite.getClientArea();
		Scrollable table = getControl(composite);
		int tableWidth = table.getSize().x;
		int trim = computeTrim(area, table, tableWidth);
		int width = Math.max(0, area.width - trim);

		if (width > 1)
			layoutTableTree(table, width, area, tableWidth < area.width);

		// For the first time we need to relayout because Scrollbars are not
		// calculate appropriately
		if (relayout) {
			relayout = false;
			composite.layout();
		}
	}

	/**
	 * Compute the area required for trim.
	 *
	 * @param area
	 * @param scrollable
	 * @param currentWidth
	 * @return int
	 */
	private int computeTrim(Rectangle area, Scrollable scrollable,
			int currentWidth) {
		int trim;

		if (currentWidth > 1) {
			trim = currentWidth - scrollable.getClientArea().width;
		} else {
			// initially, the table has no extend and no client area - use the
			// border with
			// plus some padding as educated guess
			trim = 2 * scrollable.getBorderWidth() + 1;
		}

		return trim;
	}

	/**
	 * Get the control being laid out.
	 *
	 * @param composite
	 *            the composite with the layout
	 * @return {@link Scrollable}
	 */
	Scrollable getControl(Composite composite) {
		return (Scrollable) composite.getChildren()[0];
	}

	/**
	 * Get the number of columns for the receiver.
	 *
	 * @param tableTree
	 *            the control
	 *
	 * @return the number of columns
	 * @since 3.5
	 */
	protected abstract int getColumnCount(Scrollable tableTree);

	/**
	 * Set the widths of the columns.
	 *
	 * @param tableTree
	 *            the control
	 *
	 * @param widths
	 *            the widths of the column
	 * @since 3.5
	 */
	protected abstract void setColumnWidths(Scrollable tableTree, int[] widths);

	/**
	 * Get the layout data for a column
	 *
	 * @param tableTree
	 *            the control
	 * @param columnIndex
	 *            the column index
	 * @return the layout data, might <b>not</b> null
	 * @since 3.5
	 */
	protected abstract ColumnLayoutData getLayoutData(Scrollable tableTree,
			int columnIndex);

	/**
	 * Update the layout data for a column
	 *
	 * @param column
	 *            the column
	 * @since 3.5
	 */
	protected abstract void updateColumnData(Widget column);

	/**
	 * The number of extra pixels taken as horizontal trim by the table column.
	 * To ensure there are N pixels available for the content of the column,
	 * assign N+COLUMN_TRIM for the column width.
	 *
	 * @return the trim used by the columns
	 * @since 3.4
	 */
	protected int getColumnTrim() {
		return COLUMN_TRIM;
	}
}
