/*******************************************************************************
 * Copyright (c) 2005, 2012 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.osgi.framework.util;

import java.io.File;

/**
 * A utility class for manipulating file system paths.
 * <p>
 * This class is not intended to be subclassed by clients but
 * may be instantiated.
 * </p>
 *
 * @since 3.1
 */
public class FilePath {
	// Constant value indicating if the current platform is Windows
	private static final boolean WINDOWS = java.io.File.separatorChar == '\\';
	private final static String CURRENT_DIR = "."; //$NON-NLS-1$
	// Device separator character constant ":" used in paths.
	private static final char DEVICE_SEPARATOR = ':';
	private static final byte HAS_LEADING = 1;
	private static final byte HAS_TRAILING = 4;
	// Constant value indicating no segments
	private static final String[] NO_SEGMENTS = new String[0];
	private final static String PARENT_DIR = ".."; //$NON-NLS-1$
	private final static char SEPARATOR = '/';
	private final static String UNC_SLASHES = "//"; //$NON-NLS-1$
	// if UNC, device will be \\host\share, otherwise, it will be letter/name + colon
	private String device;
	private byte flags;
	private String[] segments;

	/**
	 * Constructs a new file path from the given File object.
	 *
	 * @param location
	 */
	public FilePath(File location) {
		initialize(location.getPath());
		if (location.isDirectory())
			flags |= HAS_TRAILING;
		else
			flags &= ~HAS_TRAILING;
	}

	/**
	 * Constructs a new file path from the given string path.
	 *
	 * @param original
	 */
	public FilePath(String original) {
		initialize(original);
	}

	/*
	 * Returns the number of segments in the given path
	 */
	private int computeSegmentCount(String path) {
		int len = path.length();
		if (len == 0 || (len == 1 && path.charAt(0) == SEPARATOR))
			return 0;
		int count = 1;
		int prev = -1;
		int i;
		while ((i = path.indexOf(SEPARATOR, prev + 1)) != -1) {
			if (i != prev + 1 && i != len)
				++count;
			prev = i;
		}
		if (path.charAt(len - 1) == SEPARATOR)
			--count;
		return count;
	}

	/*
	 * Splits the given path string into an array of segments.
	 */
	private String[] computeSegments(String path) {
		int maxSegmentCount = computeSegmentCount(path);
		if (maxSegmentCount == 0)
			return NO_SEGMENTS;
		String[] newSegments = new String[maxSegmentCount];
		int len = path.length();
		// allways absolute
		int firstPosition = isAbsolute() ? 1 : 0;
		int lastPosition = hasTrailingSlash() ? len - 2 : len - 1;
		// for non-empty paths, the number of segments is
		// the number of slashes plus 1, ignoring any leading
		// and trailing slashes
		int next = firstPosition;
		int actualSegmentCount = 0;
		for (int i = 0; i < maxSegmentCount; i++) {
			int start = next;
			int end = path.indexOf(SEPARATOR, next);
			next = end + 1;
			String segment = path.substring(start, end == -1 ? lastPosition + 1 : end);
			if (CURRENT_DIR.equals(segment))
				continue;
			if (PARENT_DIR.equals(segment)) {
				if (actualSegmentCount > 0)
					actualSegmentCount--;
				continue;
			}
			newSegments[actualSegmentCount++] = segment;
		}
		if (actualSegmentCount == newSegments.length)
			return newSegments;
		if (actualSegmentCount == 0)
			return NO_SEGMENTS;
		String[] actualSegments = new String[actualSegmentCount];
		System.arraycopy(newSegments, 0, actualSegments, 0, actualSegments.length);
		return actualSegments;
	}

	/**
	 * Returns the device for this file system path, or <code>null</code> if
	 * none exists. The device string ends with a colon.
	 *
	 * @return the device string or null
	 */
	public String getDevice() {
		return device;
	}

	/**
	 * Returns the segments in this path. If this path has no segments, returns an empty array.
	 *
	 * @return an array containing all segments for this path
	 */
	public String[] getSegments() {
		return segments.clone();
	}

	/**
	 * Returns whether this path ends with a slash.
	 *
	 * @return <code>true</code> if the path ends with a slash, false otherwise
	 */
	public boolean hasTrailingSlash() {
		return (flags & HAS_TRAILING) != 0;
	}

	private void initialize(String original) {
		original = original.indexOf('\\') == -1 ? original : original.replace('\\', SEPARATOR);
		if (WINDOWS) {
			// only deal with devices/UNC paths on Windows
			int deviceSeparatorPos = original.indexOf(DEVICE_SEPARATOR);
			if (deviceSeparatorPos >= 0) {
				//extract device if any
				//remove leading slash from device part to handle output of URL.getFile()
				int start = original.charAt(0) == SEPARATOR ? 1 : 0;
				device = original.substring(start, deviceSeparatorPos + 1);
				original = original.substring(deviceSeparatorPos + 1, original.length());
			} else if (original.startsWith(UNC_SLASHES)) {
				// handle UNC paths
				int uncPrefixEnd = original.indexOf(SEPARATOR, 2);
				if (uncPrefixEnd >= 0)
					uncPrefixEnd = original.indexOf(SEPARATOR, uncPrefixEnd + 1);
				if (uncPrefixEnd >= 0) {
					device = original.substring(0, uncPrefixEnd);
					original = original.substring(uncPrefixEnd, original.length());
				} else
					// not a valid UNC
					throw new IllegalArgumentException("Not a valid UNC: " + original); //$NON-NLS-1$
			}
		}
		// device names letters and UNCs properly stripped off
		if (original.charAt(0) == SEPARATOR)
			flags |= HAS_LEADING;
		if (original.charAt(original.length() - 1) == SEPARATOR)
			flags |= HAS_TRAILING;
		segments = computeSegments(original);
	}

	/**
	 * Returns whether this path is absolute (begins with a slash).
	 *
	 * @return <code>true</code> if this path is absolute, <code>false</code> otherwise
	 */
	public boolean isAbsolute() {
		return (flags & HAS_LEADING) != 0;
	}

	/**
	 * Returns a string representing this path as a relative to the given base path.
	 * <p>
	 * If this path and the given path do not use the same device letter, this path's
	 * string representation is returned as is.
	 * </p>
	 *
	 * @param base the path this path should be made relative to
	 * @return a string representation for this path as relative to the given base path
	 */
	public String makeRelative(FilePath base) {
		if (base.device != null && !base.device.equalsIgnoreCase(this.device))
			return base.toString();
		int baseCount = this.segments.length;
		int count = this.matchingFirstSegments(base);
		if (baseCount == count && count == base.segments.length)
			return base.hasTrailingSlash() ? ("." + SEPARATOR) : "."; //$NON-NLS-1$ //$NON-NLS-2$
		StringBuilder relative = new StringBuilder(); //
		for (int j = 0; j < baseCount - count; j++)
			relative.append(PARENT_DIR + SEPARATOR);
		for (int i = 0; i < base.segments.length - count; i++) {
			relative.append(base.segments[count + i]);
			relative.append(SEPARATOR);
		}
		if (!base.hasTrailingSlash())
			relative.deleteCharAt(relative.length() - 1);
		return relative.toString();
	}

	/*
	 * Returns the number of segments in this matching the first segments of the
	 * given path.
	 */
	private int matchingFirstSegments(FilePath anotherPath) {
		int anotherPathLen = anotherPath.segments.length;
		int max = Math.min(segments.length, anotherPathLen);
		int count = 0;
		for (int i = 0; i < max; i++) {
			if (!segments[i].equals(anotherPath.segments[i]))
				return count;
			count++;
		}
		return count;
	}

	/**
	 * Returns a string representation of this path.
	 *
	 * @return  a string representation of this path
	 */
	@Override
	public String toString() {
		StringBuilder result = new StringBuilder();
		if (device != null)
			result.append(device);
		if (isAbsolute())
			result.append(SEPARATOR);
		for (String segment : segments) {
			result.append(segment);
			result.append(SEPARATOR);
		}
		if (segments.length > 0 && !hasTrailingSlash())
			result.deleteCharAt(result.length() - 1);
		return result.toString();
	}
}
