<?php
/**
 * Copyright (c) 2015 Ori Livneh <ori@wikimedia.org>
 *
 * 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 NONINFRINGEMENT.
 * 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.
 *
 * @file
 * @author Ori Livneh <ori@wikimedia.org>
 */

namespace Wikimedia;

class RelPath {
	/**
	 * Split a path into path components.
	 *
	 * @param string $path File path.
	 * @return array Array of path components.
	 */
	public static function splitPath( $path ) {
		$fragments = [];

		while ( true ) {
			$cur = dirname( $path );
			if ( $cur[0] === DIRECTORY_SEPARATOR ) {
				// dirname() on Windows sometimes returns a leading backslash, but other
				// times retains the leading forward slash. Slashes other than the leading one
				// are returned as-is, and therefore do not need to be touched.
				// Furthermore, don't break on *nix where \ is allowed in file/directory names.
				$cur[0] = '/';
			}

			if ( $cur === $path || ( $cur === '.' && basename( $path ) === $path ) ) {
				break;
			}

			$fragment = trim( substr( $path, strlen( $cur ) ), '/' );

			if ( !$fragments ) {
				$fragments[] = $fragment;
			} elseif ( $fragment === '..' && basename( $cur ) !== '..' ) {
				$cur = dirname( $cur );
			} elseif ( $fragment !== '.' ) {
				$fragments[] = $fragment;
			}

			$path = $cur;
		}

		if ( $path !== '' ) {
			$fragments[] = trim( $path, '/' );
		}

		return array_reverse( $fragments );
	}

	/**
	 * Return a relative filepath to path either from the current directory or from
	 * an optional start directory. Both paths must be absolute.
	 *
	 * @param string $path File path.
	 * @param string $start Start directory. Optional; if not specified, the current
	 *  working directory will be used.
	 * @return string|bool Relative path, or false if input was invalid.
	 */
	public static function getRelativePath( $path, $start = null ) {
		if ( $start === null ) {
			// @codeCoverageIgnoreStart
			$start = getcwd();
		}
		// @codeCoverageIgnoreEnd

		if ( substr( $path, 0, 1 ) !== '/' || substr( $start, 0, 1 ) !== '/' ) {
			return false;
		}

		$pathParts = self::splitPath( $path );
		$countPathParts = count( $pathParts );

		$startParts = self::splitPath( $start );
		$countStartParts = count( $startParts );

		$commonLength = min( $countPathParts, $countStartParts );
		for ( $i = 0; $i < $commonLength; $i++ ) {
			if ( $startParts[$i] !== $pathParts[$i] ) {
				break;
			}
		}

		$relList = ( $countStartParts > $i )
			? array_fill( 0, $countStartParts - $i, '..' )
			: [];

		$relList = array_merge( $relList, array_slice( $pathParts, $i ) );

		return implode( '/', $relList ) ?: '.';
	}

	/**
	 * Join path components.
	 *
	 * @param string $base Base path.
	 * @param string $path File path to join to base path.
	 * @return string
	 */
	public static function joinPath( $base, $path ) {
		if ( substr( $path, 0, 1 ) === '/' ) {
			// $path is absolute.
			return $path;
		}

		if ( substr( $base, 0, 1 ) !== '/' ) {
			// $base is relative.
			return false;
		}

		$pathParts = self::splitPath( $path );
		$resultParts = self::splitPath( $base );

		while ( ( $part = array_shift( $pathParts ) ) !== null ) {
			switch ( $part ) {
			case '.':
				break;
			case '..':
				if ( count( $resultParts ) > 1 ) {
					array_pop( $resultParts );
				}
				break;
			default:
				$resultParts[] = $part;
				break;
			}
		}

		return implode( '/', $resultParts );
	}
}
