<?php
namespace SabaiApps\Directories\Component\System\Helper;

use SabaiApps\Directories\Application;
use SabaiApps\Directories\Exception;

/**
 * UtilHelper provides various utility methods for system and UI operations.
 */
class UtilHelper {

	/**
	 * Returns cache duration options for forms.
	 *
	 * @param Application $application
	 * @param mixed       $value
	 * @param array|null  $possibleValues
	 * @param array|null  $excludeValues
	 * @param string|null $title
	 * @return array
	 */
	public function cacheSettingsForm( Application $application, $value = null, ?array $possibleValues = null, ?array $excludeValues = null, $title = null ) {
		$options = array( '' => __( 'No cache', 'directories-pro' ) );
		foreach ( array( 1, 2, 5, 10, 30 ) as $min ) {
			$options[ $min * 60 ] = sprintf( _n( '%d minute', '%d minutes', $min, 'directories-pro' ), $min );
		}
		foreach ( array( 1, 2, 5, 10 ) as $hour ) {
			$options[ $hour * 3600 ] = sprintf( _n( '%d hour', '%d hours', $hour, 'directories-pro' ), $hour );
		}
		foreach ( array( 1, 2, 5, 10, 30 ) as $day ) {
			$options[ $day * 86400 ] = sprintf( _n( '%d day', '%d days', $day, 'directories-pro' ), $day );
		}
		if ( isset( $possibleValues ) ) {
			$options = array_intersect_key( $options, array_flip( $possibleValues ) );
		}
		if ( isset( $excludeValues ) ) {
			$options = array_diff_key( $options, array_flip( $excludeValues ) );
		}

		return array(
			'#title'         => isset( $title ) ? $title : __( 'Cache output', 'directories-pro' ),
			'#type'          => 'select',
			'#options'       => $options,
			'#default_value' => $value,
			'#horizontal'    => true,
		);
	}

	/**
	 * Converts a string with size suffix to bytes.
	 *
	 * @param Application $application
	 * @param string|int  $str
	 * @return int
	 */
	public function strToBytes( Application $application, $str ) {
		if ( is_int( $str ) ) {
			return $str;
		}

		$suffix = strtoupper( substr( $str, -1 ) );
		if ( ! in_array( $suffix, array( 'P', 'T', 'G', 'M', 'K' ) ) ) {
			return (int) $str;
		}

		$value = (int) substr( $str, 0, -1 );
		switch ( $suffix ) {
			case 'P':
				$value *= 1024;
			case 'T':
				$value *= 1024;
			case 'G':
				$value *= 1024;
			case 'M':
				$value *= 1024;
			case 'K':
				$value *= 1024;
				break;
		}
		return $value;
	}

	/**
	 * Converts bytes to a human-readable string.
	 *
	 * @param Application $application
	 * @param int         $bytes
	 * @param int         $decimals
	 * @return string
	 */
	public function bytesToStr( Application $application, $bytes, $decimals = 1 ) {
		$suffix      = 'B';
		$suffix_list = array( 'PB', 'TB', 'GB', 'MB', 'KB' );
		while ( $bytes > 1024
			&& ( $suffix = array_pop( $suffix_list ) )
		) {
			$bytes /= 1024;
		}
		return round( $bytes, $decimals ) . $suffix;
	}

	/**
	 * Returns a formatted string of available tags.
	 *
	 * @param Application $application
	 * @param array       $tags
	 * @param string      $openTag
	 * @param string      $closeTag
	 * @return string
	 */
	public function availableTags( Application $application, array $tags, $openTag = '', $closeTag = '' ) {
		return sprintf(
			$application->H( __( 'Available tags: %s', 'directories-pro' ) ),
			'<code class="' . DRTS_BS_PREFIX . 'user-select-all">' . $openTag . implode( $closeTag . '</code>, <code class="' . DRTS_BS_PREFIX . 'user-select-all">' . $openTag, array_map( array( $application, 'H' ), $tags ) ) . $closeTag . '</code>'
		);
	}

	/**
	 * Returns size options for UI elements.
	 *
	 * @param Application $application
	 * @param bool        $includeNegative
	 * @return array
	 */
	public function sizeOptions( Application $application, $includeNegative = false ) {
		$suffix = $includeNegative ? ' (+)' : '';
		$sizes  = array(
			0 => __( 'None', 'directories-pro' ),
			1 => __( 'X-Small', 'directories-pro' ) . $suffix,
			2 => __( 'Small', 'directories-pro' ) . $suffix,
			3 => __( 'Medium', 'directories-pro' ) . $suffix,
			4 => __( 'Large', 'directories-pro' ) . $suffix,
			5 => __( 'X-Large', 'directories-pro' ) . $suffix,
		);
		if ( $includeNegative ) {
			$sizes[-1] = __( 'X-Small', 'directories-pro' ) . ' (-)';
			$sizes[-2] = __( 'Small', 'directories-pro' ) . ' (-)';
			$sizes[-3] = __( 'Medium', 'directories-pro' ) . ' (-)';
			$sizes[-4] = __( 'Large', 'directories-pro' ) . ' (-)';
			$sizes[-5] = __( 'X-Large', 'directories-pro' ) . ' (-)';
		}
		return $sizes;
	}

	/**
	 * Gets the MIME type of a file.
	 *
	 * @param Application $application
	 * @param string      $file
	 * @param bool        $isImage
	 * @return string
	 * @throws Exception\RuntimeException
	 */
	public function fileType( Application $application, $file, $isImage = false ) {
		if ( $isImage ) {
			if ( $size = @getimagesize( $file ) ) {
				return $size['mime'];
			}
		}
		if ( function_exists( 'finfo_file' ) ) {
			if ( $finfo = @finfo_open( FILEINFO_MIME ) ) {
				$mime = finfo_file( $finfo, $file );
				@finfo_close( $finfo );
				if ( $mime ) {
					return $mime;
				}
			}
		}
		if ( ! function_exists( 'mime_content_type' ) ) {
			throw new Exception\RuntimeException( 'Could not find finfo_file or mime_content_type function' );
		}

		return mime_content_type( $file );
	}

	/**
	 * Validates that a directory exists and is writeable.
	 * Creates the directory if it does not exist.
	 *
	 * @param Application $application
	 * @param string      $dir Directory path.
	 * @param bool        $ensureWriteable Whether to ensure the directory is writeable.
	 * @param int         $writeableMode Permissions to set if making writeable.
	 * @param bool        $recursive Whether to create directories recursively.
	 * @return string The validated directory path.
	 * @throws Exception\RuntimeException If the directory cannot be created or made writeable.
	 */
	public function validateDirectory( Application $application, $dir, $ensureWriteable = false, $writeableMode = 0755, $recursive = true ) {
		$dir = $application->getPath( $dir );
		if ( ! is_dir( $dir ) && ! @mkdir( $dir, $writeableMode, $recursive ) ) {
			throw new Exception\RuntimeException( sprintf( __( 'Folder %s does not exist.', 'directories-pro' ), $dir ) );
		}
		if ( $ensureWriteable && ! is_writeable( $dir ) && ! @chmod( $dir, $writeableMode ) ) {
			throw new Exception\RuntimeException( sprintf( __( 'Folder %s is not writeable by the server.', 'directories-pro' ), $dir ) );
		}
		return $dir;
	}

	/**
	 * Outputs a redirect message and button, and triggers a JavaScript redirect after a delay.
	 *
	 * @param Application $application
	 * @param string      $url The URL to redirect to.
	 * @param string      $html Optional HTML message to display.
	 * @param int         $delay Delay in milliseconds before redirect.
	 */
	public function redirectHtml( Application $application, $url, $html = '', $delay = 2000 ) {
		printf(
			'<script type="text/javascript">document.addEventListener("DOMContentLoaded", function(e) {
    setTimeout(function () {
        window.location.replace("%2$s");
    }, %3$d);
});</script>
%4$s
<div class="drts-redirect-btn">
    <p>%5$s</p>
    <div>
        <a href="%2$s" class="%1$sbtn %1$sbtn-secondary">%6$s</a>
    </div>
</div>',
			DRTS_BS_PREFIX,
			$url,
			$delay,
			strlen( $html ) ? '<div class="drts-redirect-msg">' . $html . '</div>' : '',
			$application->H( __( 'Redirecting... If you are not redirected automatically, please click the button below to continue.', 'directories-pro' ) ),
			$application->H( __( 'Continue &raquo;', 'directories-pro' ) )
		);
	}

	/**
	 * Slices an array into the specified number of columns.
	 *
	 * @param Application $application
	 * @param array       $arr The array to slice.
	 * @param int         $columnCount Number of columns.
	 * @param bool        $vertical Whether to slice vertically (default true).
	 * @return array
	 */
	public function sliceArray( Application $application, array $arr, $columnCount, $vertical = true ) {
		if ( $columnCount < 1
			|| ( ! $entity_count = count( $arr ) )
		) {
			return array();
		}

		$ret = array();
		if ( $vertical ) {
			$additional_entity_count = $entity_count % $columnCount;
			$entity_count_per_slice  = floor( $entity_count / $columnCount );
			$last_slice_index        = $columnCount - 1;
			$sliced_count            = 0;
			for ( $i = 0; $i <= $last_slice_index; $i++ ) {
				if ( $additional_entity_count ) {
					--$additional_entity_count;
					$slice_entity_count = $entity_count_per_slice + 1;
				} else {
					$slice_entity_count = $entity_count_per_slice;
				}
				$slice         = array_slice( $arr, $sliced_count, $slice_entity_count, true );
				$ret[]         = $slice;
				$sliced_count += $slice_entity_count;
			}
		} else {
			$entity_count_per_slice = $columnCount;
			$last_slice_index       = ceil( $entity_count / $columnCount ) - 1;
			for ( $i = 0; $i <= $last_slice_index; $i++ ) {
				$ret[ $i ] = array_slice( $arr, $i * $entity_count_per_slice, $i === $last_slice_index ? null : $entity_count_per_slice, true );
			}
		}
		return $ret;
	}

	/**
	 * Calls the given callback with the provided parameters.
	 *
	 * @param Application    $application
	 * @param callable|array $callback Callback function or array.
	 * @param array          $params Parameters to pass to the callback.
	 * @return mixed
	 */
	public function callUserFuncArray( Application $application, $callback, array $params = array() ) {
		if ( is_array( $callback ) && isset( $callback[1] ) && is_array( $callback[1] ) ) {
			$params   = empty( $params ) ? $callback[1] : array_merge( $params, $callback[1] );
			$callback = $callback[0];
		}

		return call_user_func_array( $callback, $params );
	}

	/**
	 * Encodes a variable as JSON.
	 *
	 * @param Application $application
	 * @param mixed       $var Variable to encode.
	 * @param int|null    $option Optional json_encode options.
	 * @return string
	 */
	public function jsonEncode( Application $application, $var, $option = null ) {
		$json = json_encode( $var, $option ?? ( JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ) );
		if ( json_last_error() !== JSON_ERROR_NONE ) {
			$application->logError(
				sprintf( __( 'JSON encode error: %s', 'directories-pro' ), json_last_error_msg() ),
				array(
					'var'    => $var,
					'option' => $option,
				)
			);
			throw new \RuntimeException( 'JSON encode error: ' . json_last_error_msg() );
		}

		return $json;
	}

	/**
	 * Returns time options for forms.
	 *
	 * @param Application $application
	 * @param int|null    $value
	 * @param array       $options
	 * @return array
	 */
	public function timeOptions( Application $application, $value = null, array $options = array() ) {
		$options += array(
			'format'      => null,
			'step'        => 15,
			'html'        => false,
			'placeholder' => '--',
			'skip'        => array(), // skip range
		);
		if ( isset( $value )
			&& $value > 86400
		) {
			$value %= 86400;
		}
		if ( ! empty( $options['format'] ) ) {
			if ( $options['format'] === 24 ) {
				$format = 'H:i';
			} elseif ( $options['format'] === 12 ) {
				$format = 'g:i A';
			} else {
				$format = $options['format'];
			}
		} else {
			$format = $application->getPlatform()->getTimeFormat();
		}
		if ( ! $step = intval( $options['step'] ) ) {
			$step = 15;
		}
		$times       = array();
		$is_selected = false;
		for ( $i = 0; $i < 24; ++$i ) {
			for ( $j = 0; $j < 60; $j += $step ) {
				$option_value = $i * 3600 + $j * 60;
				$is_disabled  = isset( $options['skip'][0] ) && isset( $options['skip'][1] ) && false !== filter_var(
					$option_value,
					FILTER_VALIDATE_INT,
					array(
						'options' => array(
							'min_range' => $options['skip'][0],
							'max_range' => $options['skip'][1],
						),
					)
				);
				if ( ! $is_disabled
					&& ! $is_selected
					&& $value === $option_value
				) {
					$selected    = ' selected="selected"';
					$is_selected = true;
					$disabled    = '';
				} else {
					$selected = '';
					$disabled = $is_disabled ? ' disabled="disabled"' : '';
				}
				$label = date( $format, mktime( $i, $j, 0 ) );
				if ( ! empty( $options['html'] ) ) {
					$label = '<option value="' . $option_value . '"' . $selected . $disabled . '>' . $application->H( $label ) . '</option>';
				}
				$times[ $option_value ] = $label;
			}
		}
		// Add option for current value is not in options
		if ( isset( $value )
			&& ! $is_selected
		) {
			if ( $value % 60 ) {
				$value -= $value % 60;
			}
			$label = date( $format, mktime( intval( $value / 3600 ), intval( ( $value % 3600 ) / 60 ), 0 ) );
			if ( ! empty( $options['html'] ) ) {
				$label = '<option value="' . $value . '" selected="selected">' . $application->H( $label ) . '</option>';
			}
			$times[ $value ] = $label;
		}
		ksort( $times );
		// Add placeholder?
		if ( isset( $options['placeholder'] ) ) {
			$label = $options['placeholder'];
			if ( ! empty( $options['html'] ) ) {
				$label = '<option value="" selected="selected">' . $application->H( $label ) . '</option>';
			}
			$times = array( '' => $label ) + $times;
		}

		return $times;
	}

	/**
	 * Checks if the system supports MX record checking for email validation.
	 *
	 * @return bool
	 */
	public function canCheckMx( Application $application ) {
		return function_exists( 'checkdnsrr' )
			&& version_compare( PHP_VERSION, '5.2.4', '>=' ); // 2nd parameter of checkdnsrr added in 5.2.4
	}
}
