<?php

namespace JmesPath;

use ArrayAccess;
use Closure;
use Countable;
use InvalidArgumentException;
use RuntimeException;
use stdClass;

class Utils {
	static $typeMap = [
		'boolean' => 'boolean',
		'string'  => 'string',
		'NULL'    => 'null',
		'double'  => 'number',
		'float'   => 'number',
		'integer' => 'number'
	];

	/**
	 * Returns true if the value is truthy
	 *
	 * @param mixed $value Value to check
	 *
	 * @return bool
	 */
	public static function isTruthy( $value ) {
		if ( ! $value ) {
			return $value === 0 || $value === '0';
		} elseif ( $value instanceof stdClass ) {
			return (bool) get_object_vars( $value );
		} else {
			return true;
		}
	}

	/**
	 * Gets the JMESPath type equivalent of a PHP variable.
	 *
	 * @param mixed $arg PHP variable
	 *
	 * @return string Returns the JSON data type
	 * @throws InvalidArgumentException when an unknown type is given.
	 */
	public static function type( $arg ) {
		$type = gettype( $arg );
		if ( isset( self::$typeMap[ $type ] ) ) {
			return self::$typeMap[ $type ];
		} elseif ( $type === 'array' ) {
			if ( empty( $arg ) ) {
				return 'array';
			}
			reset( $arg );

			return key( $arg ) === 0 ? 'array' : 'object';
		} elseif ( $arg instanceof stdClass ) {
			return 'object';
		} elseif ( $arg instanceof Closure ) {
			return 'expression';
		} elseif ( $arg instanceof ArrayAccess
		           && $arg instanceof Countable
		) {
			return count( $arg ) == 0 || $arg->offsetExists( 0 )
				? 'array'
				: 'object';
		} elseif ( method_exists( $arg, '__toString' ) ) {
			return 'string';
		}

		throw new InvalidArgumentException(
			'Unable to determine JMESPath type from ' . get_class( $arg )
		);
	}

	/**
	 * Determine if the provided value is a JMESPath compatible object.
	 *
	 * @param mixed $value
	 *
	 * @return bool
	 */
	public static function isObject( $value ) {
		if ( is_array( $value ) ) {
			return ! $value || array_keys( $value )[0] !== 0;
		}

		// Handle array-like values. Must be empty or offset 0 does not exist
		return $value instanceof Countable && $value instanceof ArrayAccess
			? count( $value ) == 0 || ! $value->offsetExists( 0 )
			: $value instanceof stdClass;
	}

	/**
	 * Determine if the provided value is a JMESPath compatible array.
	 *
	 * @param mixed $value
	 *
	 * @return bool
	 */
	public static function isArray( $value ) {
		if ( is_array( $value ) ) {
			return ! $value || array_keys( $value )[0] === 0;
		}

		// Handle array-like values. Must be empty or offset 0 exists.
		return $value instanceof Countable && $value instanceof ArrayAccess
			? count( $value ) == 0 || $value->offsetExists( 0 )
			: false;
	}

	/**
	 * JSON aware value comparison function.
	 *
	 * @param mixed $a First value to compare
	 * @param mixed $b Second value to compare
	 *
	 * @return bool
	 */
	public static function isEqual( $a, $b ) {
		if ( $a === $b ) {
			return true;
		} elseif ( $a instanceof stdClass ) {
			return self::isEqual( (array) $a, $b );
		} elseif ( $b instanceof stdClass ) {
			return self::isEqual( $a, (array) $b );
		} else {
			return false;
		}
	}

	/**
	 * JMESPath requires a stable sorting algorithm, so here we'll implement
	 * a simple Schwartzian transform that uses array index positions as tie
	 * breakers.
	 *
	 * @param array $data List or map of data to sort
	 * @param callable $sortFn Callable used to sort values
	 *
	 * @return array Returns the sorted array
	 * @link http://en.wikipedia.org/wiki/Schwartzian_transform
	 */
	public static function stableSort( array $data, callable $sortFn ) {
		// Decorate each item by creating an array of [value, index]
		array_walk( $data, function ( &$v, $k ) {
			$v = [ $v, $k ];
		} );
		// Sort by the sort function and use the index as a tie-breaker
		uasort( $data, function ( $a, $b ) use ( $sortFn ) {
			return $sortFn( $a[0], $b[0] ) ?: ( $a[1] < $b[1] ? - 1 : 1 );
		} );

		// Undecorate each item and return the resulting sorted array
		return array_map( function ( $v ) {
			return $v[0];
		}, array_values( $data ) );
	}

	/**
	 * Creates a Python-style slice of a string or array.
	 *
	 * @param array|string $value Value to slice
	 * @param int|null $start Starting position
	 * @param int|null $stop Stop position
	 * @param int $step Step (1, 2, -1, -2, etc.)
	 *
	 * @return array|string
	 * @throws InvalidArgumentException
	 */
	public static function slice( $value, $start = null, $stop = null, $step = 1 ) {
		if ( ! is_array( $value ) && ! is_string( $value ) ) {
			throw new InvalidArgumentException( 'Expects string or array' );
		}

		return self::sliceIndices( $value, $start, $stop, $step );
	}

	private static function sliceIndices( $subject, $start, $stop, $step ) {
		$type = gettype( $subject );
		$len  = $type == 'string' ? strlen( $subject ) : count( $subject );
		list( $start, $stop, $step ) = self::adjustSlice( $len, $start, $stop, $step );

		$result = [];
		if ( $step > 0 ) {
			for ( $i = $start; $i < $stop; $i += $step ) {
				$result[] = $subject[ $i ];
			}
		} else {
			for ( $i = $start; $i > $stop; $i += $step ) {
				$result[] = $subject[ $i ];
			}
		}

		return $type == 'string' ? implode( $result, '' ) : $result;
	}

	private static function adjustSlice( $length, $start, $stop, $step ) {
		if ( $step === null ) {
			$step = 1;
		} elseif ( $step === 0 ) {
			throw new RuntimeException( 'step cannot be 0' );
		}

		if ( $start === null ) {
			$start = $step < 0 ? $length - 1 : 0;
		} else {
			$start = self::adjustEndpoint( $length, $start, $step );
		}

		if ( $stop === null ) {
			$stop = $step < 0 ? - 1 : $length;
		} else {
			$stop = self::adjustEndpoint( $length, $stop, $step );
		}

		return [ $start, $stop, $step ];
	}

	private static function adjustEndpoint( $length, $endpoint, $step ) {
		if ( $endpoint < 0 ) {
			$endpoint += $length;
			if ( $endpoint < 0 ) {
				$endpoint = $step < 0 ? - 1 : 0;
			}
		} elseif ( $endpoint >= $length ) {
			$endpoint = $step < 0 ? $length - 1 : $length;
		}

		return $endpoint;
	}
}
