/*******************************************************************************
 * Copyright (c) 2005, 2008 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.ltk.ui.refactoring.history;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.eclipse.core.runtime.Assert;

import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;

import org.eclipse.ltk.core.refactoring.RefactoringDescriptorProxy;
import org.eclipse.ltk.core.refactoring.history.RefactoringHistory;
import org.eclipse.ltk.internal.ui.refactoring.history.RefactoringHistoryCollection;
import org.eclipse.ltk.internal.ui.refactoring.history.RefactoringHistoryDate;
import org.eclipse.ltk.internal.ui.refactoring.history.RefactoringHistoryEntry;
import org.eclipse.ltk.internal.ui.refactoring.history.RefactoringHistoryNode;

/**
 * Tree content provider to display a refactoring history. This content provider
 * may be used with {@link RefactoringHistory} elements and returns model
 * elements suitable to be rendered using
 * {@link RefactoringHistoryLabelProvider}.
 * <p>
 * Note: this class is not indented to be subclassed outside the refactoring
 * framework.
 * </p>
 *
 * @see IRefactoringHistoryControl
 * @see RefactoringHistoryControlConfiguration
 *
 * @since 3.2
 *
 * @noextend This class is not intended to be subclassed by clients.
 */
public class RefactoringHistoryContentProvider implements ITreeContentProvider {

	/** The no elements constant */
	private static final Object[] NO_ELEMENTS= {};

	/**
	 * Implements a binary search which computes the index of the number, or a
	 * nearest approximative index if the number has not been found.
	 * <p>
	 * The array must be sorted in descending order.
	 * </p>
	 *
	 * @param array
	 *            the array, sorted in descending order, to search for the
	 *            number
	 * @param number
	 *            the number to search for
	 * @return the index of the number in the array, or the nearest index,
	 *         depending on <code>right</code>
	 */
	private static int binarySearch(final long[] array, final long number) {
		int left= 0;
		int right= array.length - 1;
		int median;
		do {
			median= (left + right) / 2;
			if (number > array[median])
				right= median - 1;
			else
				left= median + 1;
		} while (number != array[median] && left <= right);
		if (number == array[median])
			return median;
		return left;
	}

	/**
	 * Returns the index of the specified root kind in the structure.
	 *
	 * @param structure
	 *            the structure
	 * @param kind
	 *            the root kind
	 * @return the index, or <code>-1</code>
	 */
	private static int getRefactoringRootKindIndex(final long[][] structure, final int kind) {
		for (int index= structure.length - 1; index >= 0; index--) {
			if (kind >= structure[index][1])
				return index;
		}
		return -1;
	}

	/** The refactoring history control configuration to use */
	private final RefactoringHistoryControlConfiguration fControlConfiguration;

	/** The refactoring history, or <code>null</code> */
	private RefactoringHistory fRefactoringHistory= null;

	/** The refactoring root structure, or <code>null</code> */
	private long[][] fRefactoringRoots= null;

	/** The refactoring time stamps, in descending order, or <code>null</code> */
	private long[] fRefactoringStamps= null;

	/**
	 * Creates a new refactoring history content provider.
	 *
	 * @param configuration
	 *            the refactoring history control configuration
	 */
	public RefactoringHistoryContentProvider(final RefactoringHistoryControlConfiguration configuration) {
		Assert.isNotNull(configuration);
		fControlConfiguration= configuration;
	}

	@Override
	public void dispose() {
		// Do nothing
	}

	@Override
	public Object[] getChildren(final Object element) {
		if (element instanceof RefactoringHistoryNode) {
			final RefactoringHistoryNode node= (RefactoringHistoryNode) element;
			final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
			if (proxies.length > 0) {
				final long[][] structure= getRefactoringRootStructure(proxies[0].getTimeStamp());
				final int kind= node.getKind();
				switch (kind) {
					case RefactoringHistoryNode.COLLECTION:
						return getRefactoringHistoryEntries(node);
					default: {
						if (node instanceof RefactoringHistoryDate) {
							final RefactoringHistoryDate date= (RefactoringHistoryDate) node;
							final long stamp= date.getTimeStamp();
							switch (kind) {
								case RefactoringHistoryNode.TODAY:
									return getRefactoringHistoryEntries(date, stamp, Long.MAX_VALUE);
								case RefactoringHistoryNode.YESTERDAY:
									return getRefactoringHistoryEntries(date, stamp, structure[getRefactoringRootKindIndex(structure, RefactoringHistoryNode.TODAY)][0] - 1);
								case RefactoringHistoryNode.THIS_WEEK:
									return getRefactoringHistoryDays(date, stamp, structure[getRefactoringRootKindIndex(structure, RefactoringHistoryNode.YESTERDAY)][0] - 1);
								case RefactoringHistoryNode.LAST_WEEK:
									return getRefactoringHistoryDays(date, stamp, structure[getRefactoringRootKindIndex(structure, RefactoringHistoryNode.THIS_WEEK)][0] - 1);
								case RefactoringHistoryNode.THIS_MONTH:
									return getRefactoringHistoryWeeks(date, stamp, structure[getRefactoringRootKindIndex(structure, RefactoringHistoryNode.LAST_WEEK)][0] - 1);
								case RefactoringHistoryNode.LAST_MONTH:
									return getRefactoringHistoryWeeks(date, stamp, structure[getRefactoringRootKindIndex(structure, RefactoringHistoryNode.THIS_MONTH)][0] - 1);
								case RefactoringHistoryNode.DAY:
									return getRefactoringHistoryEntries(date, stamp, stamp + 1000 * 60 * 60 * 24 - 1);
								case RefactoringHistoryNode.WEEK:
									return getRefactoringHistoryDays(date, stamp, stamp + 1000 * 60 * 60 * 24 * 7 - 1);
								case RefactoringHistoryNode.MONTH: {
									final Calendar calendar= Calendar.getInstance();
									calendar.setTimeInMillis(stamp);
									calendar.add(Calendar.MONTH, 1);
									return getRefactoringHistoryWeeks(date, stamp, calendar.getTimeInMillis() - 1);
								}
								case RefactoringHistoryNode.YEAR: {
									final Calendar calendar= Calendar.getInstance();
									calendar.setTimeInMillis(stamp);
									calendar.add(Calendar.YEAR, 1);
									return getRefactoringHistoryMonths(date, stamp, calendar.getTimeInMillis() - 1);
								}
							}
						}
					}
				}
			}
		} else if (element instanceof RefactoringHistory)
			return getElements(element);
		return NO_ELEMENTS;
	}

	@Override
	public Object[] getElements(final Object element) {
		if (element instanceof RefactoringHistory) {
			if (fControlConfiguration.isTimeDisplayed())
				return getRootElements();
			else if (fRefactoringHistory != null && !fRefactoringHistory.isEmpty())
				return new Object[] { new RefactoringHistoryCollection()};
		}
		return NO_ELEMENTS;
	}

	@Override
	public Object getParent(final Object element) {
		if (element instanceof RefactoringHistoryNode) {
			final RefactoringHistoryNode node= (RefactoringHistoryNode) element;
			return node.getParent();
		}
		return null;
	}

	/**
	 * Returns the refactoring descriptor proxies, sorted in descending order of
	 * their time stamps, and caches time stamp information.
	 *
	 * @return the refactoring descriptor proxies
	 */
	private RefactoringDescriptorProxy[] getRefactoringDescriptorProxies() {
		final RefactoringDescriptorProxy[] proxies;
		if (fRefactoringHistory == null)
			proxies= new RefactoringDescriptorProxy[0];
		else
			proxies= fRefactoringHistory.getDescriptors();
		if (fRefactoringStamps == null) {
			final int length= proxies.length;
			fRefactoringStamps= new long[length];
			for (int index= 0; index < length; index++)
				fRefactoringStamps[index]= proxies[index].getTimeStamp();
		}
		return proxies;
	}

	/**
	 * Returns the refactoring history days.
	 *
	 * @param parent
	 *            the parent node, or <code>null</code>
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp. inclusive
	 *
	 * @return the refactoring history days
	 */
	private Object[] getRefactoringHistoryDays(final RefactoringHistoryDate parent, final long start, final long end) {
		final long time= parent.getTimeStamp();
		final Calendar calendar= Calendar.getInstance();
		final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
		final int[] range= getRefactoringHistoryRange(start, end);
		final List<RefactoringHistoryDate> list= new ArrayList<>(proxies.length);
		int last= -1;
		for (int index= range[0]; index <= range[1]; index++) {
			long stamp= proxies[index].getTimeStamp();
			if (stamp >= time) {
				calendar.setTimeInMillis(stamp);
				final int day= calendar.get(Calendar.DAY_OF_YEAR);
				if (day != last) {
					last= day;
					calendar.set(Calendar.MILLISECOND, 0);
					calendar.set(Calendar.SECOND, 0);
					calendar.set(Calendar.MINUTE, 0);
					calendar.set(Calendar.HOUR_OF_DAY, 0);
					stamp= calendar.getTimeInMillis();
					if (stamp < time)
						list.add(new RefactoringHistoryDate(parent, time, RefactoringHistoryNode.DAY));
					else
						list.add(new RefactoringHistoryDate(parent, stamp, RefactoringHistoryNode.DAY));
				}
			}
		}
		return list.toArray();
	}

	/**
	 * Returns the refactoring history entries.
	 *
	 * @param parent
	 *            the parent node, or <code>null</code>
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp. inclusive
	 *
	 * @return the refactoring history entries
	 */
	private Object[] getRefactoringHistoryEntries(final RefactoringHistoryDate parent, final long start, final long end) {
		final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
		final int[] range= getRefactoringHistoryRange(start, end);
		final List<RefactoringHistoryEntry> list= new ArrayList<>(proxies.length);
		for (int index= range[0]; index <= range[1]; index++)
			list.add(new RefactoringHistoryEntry(parent, proxies[index]));
		return list.toArray();
	}

	/**
	 * Returns the refactoring history entries.
	 *
	 * @param parent
	 *            the parent node, or <code>null</code>
	 *
	 * @return the refactoring history entries
	 */
	private Object[] getRefactoringHistoryEntries(final RefactoringHistoryNode parent) {
		final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
		final List<RefactoringHistoryEntry> list= new ArrayList<>(proxies.length);
		for (RefactoringDescriptorProxy proxy : proxies) {
			list.add(new RefactoringHistoryEntry(parent, proxy));
		}
		return list.toArray();
	}

	/**
	 * Returns the refactoring history months.
	 *
	 * @param parent
	 *            the parent node, or <code>null</code>
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp. inclusive
	 *
	 * @return the refactoring history months
	 */
	private Object[] getRefactoringHistoryMonths(final RefactoringHistoryDate parent, final long start, final long end) {
		final long time= parent.getTimeStamp();
		final Calendar calendar= Calendar.getInstance();
		final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
		final int[] range= getRefactoringHistoryRange(start, end);
		final List<RefactoringHistoryDate> list= new ArrayList<>(proxies.length);
		int last= -1;
		for (int index= range[0]; index <= range[1]; index++) {
			long stamp= proxies[index].getTimeStamp();
			if (stamp >= time) {
				calendar.setTimeInMillis(stamp);
				final int month= calendar.get(Calendar.MONTH);
				if (month != last) {
					last= month;
					calendar.set(Calendar.MILLISECOND, 0);
					calendar.set(Calendar.SECOND, 0);
					calendar.set(Calendar.MINUTE, 0);
					calendar.set(Calendar.HOUR_OF_DAY, 0);
					calendar.set(Calendar.DAY_OF_MONTH, 1);
					stamp= calendar.getTimeInMillis();
					if (stamp < time)
						list.add(new RefactoringHistoryDate(parent, time, RefactoringHistoryNode.MONTH));
					else
						list.add(new RefactoringHistoryDate(parent, stamp, RefactoringHistoryNode.MONTH));
				}
			}
		}
		return list.toArray();
	}

	/**
	 * Returns the refactoring history range for the specified time stamps.
	 *
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp. inclusive
	 * @return An array containing the index range
	 */
	private int[] getRefactoringHistoryRange(final long start, final long end) {
		final int[] range= new int[2];
		range[1]= binarySearch(fRefactoringStamps, start) - 1;
		range[0]= binarySearch(fRefactoringStamps, end);
		return range;
	}

	/**
	 * Returns the refactoring history weeks.
	 *
	 * @param parent
	 *            the parent node, or <code>null</code>
	 * @param start
	 *            the start time stamp, inclusive
	 * @param end
	 *            the end time stamp. inclusive
	 *
	 * @return the refactoring history weeks
	 */
	private Object[] getRefactoringHistoryWeeks(final RefactoringHistoryDate parent, final long start, final long end) {
		final long time= parent.getTimeStamp();
		final Calendar calendar= Calendar.getInstance();
		final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
		final int[] range= getRefactoringHistoryRange(start, end);
		final List<RefactoringHistoryDate> list= new ArrayList<>(proxies.length);
		int last= -1;
		for (int index= range[0]; index <= range[1]; index++) {
			long stamp= proxies[index].getTimeStamp();
			if (stamp >= time) {
				calendar.setTimeInMillis(stamp);
				final int week= calendar.get(Calendar.WEEK_OF_YEAR);
				if (week != last) {
					last= week;
					calendar.set(Calendar.MILLISECOND, 0);
					calendar.set(Calendar.SECOND, 0);
					calendar.set(Calendar.MINUTE, 0);
					calendar.set(Calendar.HOUR_OF_DAY, 0);
					calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
					stamp= calendar.getTimeInMillis();
					if (stamp < time)
						list.add(new RefactoringHistoryDate(parent, time, RefactoringHistoryNode.WEEK));
					else
						list.add(new RefactoringHistoryDate(parent, stamp, RefactoringHistoryNode.WEEK));
				}
			}
		}
		return list.toArray();
	}

	/**
	 * Computes and returns the refactoring root structure if necessary.
	 *
	 * @param stamp
	 *            the time stamp of the oldest refactoring
	 * @return the refactoring root structure
	 */
	private long[][] getRefactoringRootStructure(final long stamp) {
		if (fRefactoringRoots == null) {
			final long time= System.currentTimeMillis();
			final Calendar calendar= Calendar.getInstance();
			calendar.setTimeInMillis(time);
			calendar.set(Calendar.HOUR_OF_DAY, 0);
			calendar.set(Calendar.MINUTE, 0);
			calendar.set(Calendar.SECOND, 0);
			calendar.set(Calendar.MILLISECOND, 0);
			final int zoneOffset= calendar.get(Calendar.ZONE_OFFSET);
			final int dstOffset= calendar.get(Calendar.DST_OFFSET);
			int count= 0;
			final long[] thresholds= new long[32];
			final int[] kinds= new int[32];
			thresholds[count]= calendar.getTimeInMillis();
			kinds[count]= RefactoringHistoryNode.TODAY;
			count++;
			calendar.add(Calendar.DATE, -1);
			thresholds[count]= calendar.getTimeInMillis();
			kinds[count]= RefactoringHistoryNode.YESTERDAY;
			count++;
			final int day= calendar.get(Calendar.DAY_OF_WEEK);
			if (day != Calendar.SUNDAY) {
				calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
				thresholds[count]= calendar.getTimeInMillis();
				kinds[count]= RefactoringHistoryNode.THIS_WEEK;
				count++;
				calendar.add(Calendar.WEEK_OF_YEAR, -1);
			}
			calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
			thresholds[count]= calendar.getTimeInMillis();
			kinds[count]= RefactoringHistoryNode.LAST_WEEK;
			count++;
			calendar.setTimeInMillis(time);
			calendar.set(Calendar.HOUR_OF_DAY, 0);
			calendar.set(Calendar.MINUTE, 0);
			calendar.set(Calendar.SECOND, 0);
			calendar.set(Calendar.MILLISECOND, 0);
			calendar.set(Calendar.DAY_OF_MONTH, 1);
			if (thresholds[count - 1] >= calendar.getTimeInMillis()) {
				thresholds[count]= calendar.getTimeInMillis();
				kinds[count]= RefactoringHistoryNode.THIS_MONTH;
				count++;
			}
			calendar.add(Calendar.MONTH, -1);
			thresholds[count]= calendar.getTimeInMillis();
			kinds[count]= RefactoringHistoryNode.LAST_MONTH;
			count++;
			final int month= calendar.get(Calendar.MONTH);
			if (month != 0) {
				calendar.set(Calendar.MONTH, 0);
				thresholds[count]= calendar.getTimeInMillis();
				kinds[count]= RefactoringHistoryNode.YEAR;
				count++;
			}
			if (stamp > 0) {
				final long localized= stamp + zoneOffset + dstOffset;
				calendar.set(Calendar.MONTH, 0);
				do {
					calendar.add(Calendar.YEAR, -1);
					thresholds[count]= calendar.getTimeInMillis();
					kinds[count]= RefactoringHistoryNode.YEAR;
					count++;
				} while (calendar.getTimeInMillis() > localized);
			}
			final long[][] result= new long[count][2];
			for (int index= 0; index < count; index++) {
				result[index][0]= thresholds[index];
				result[index][1]= kinds[index];
			}
			fRefactoringRoots= result;
		}
		return fRefactoringRoots;
	}

	/**
	 * Returns the refactoring history root elements.
	 * <p>
	 * This method must only be called for refactoring histories with associated
	 * time information.
	 * </p>
	 *
	 * @return the refactoring history root elements
	 */
	public Object[] getRootElements() {
		final List<RefactoringHistoryDate> list= new ArrayList<>(16);
		if (fRefactoringHistory != null && !fRefactoringHistory.isEmpty()) {
			final RefactoringDescriptorProxy[] proxies= getRefactoringDescriptorProxies();
			if (proxies.length > 0) {
				final long[][] structure= getRefactoringRootStructure(proxies[0].getTimeStamp());
				int begin= 0;
				long end= Long.MAX_VALUE;
				for (RefactoringDescriptorProxy proxy : proxies) {
					final long stamp= proxy.getTimeStamp();
					for (int offset= begin; offset < structure.length; offset++) {
						final long start= structure[offset][0];
						if (stamp >= start && stamp <= end) {
							list.add(new RefactoringHistoryDate(null, start, (int) structure[offset][1]));
							begin= offset + 1;
							end= start - 1;
							break;
						}
					}
				}
			}
		}
		return list.toArray();
	}

	@Override
	public boolean hasChildren(final Object element) {
		return !(element instanceof RefactoringHistoryEntry);
	}

	@Override
	public void inputChanged(final Viewer viewer, final Object predecessor, final Object successor) {
		if (predecessor == successor || fRefactoringHistory == successor)
			return;
		if (successor instanceof RefactoringHistory) {
			if (successor.equals(fRefactoringHistory))
				return;
			fRefactoringHistory= (RefactoringHistory) successor;
		} else
			fRefactoringHistory= null;
		fRefactoringRoots= null;
		fRefactoringStamps= null;
	}
}
