<?php
/**
 * economy.php
 *
 * Provides functions required by helpers
 *
 * @package     magicoli/opensim-helpers
 * @author      Gudule Lapointe <gudule@speculoos.world>
 * @link            https://github.com/magicoli/opensim-helpers
 * @license     AGPLv3
 */

/**
 * Verify if given string is an UUID.
 * In theory, we would check want v4-compliant uuids
 * (xxxxxxxx-xxxx-4xxx-[89AB]xxx-xxxxxxxxxxxx) but OpenSimulator seems to have
 * lot of non v4-compliant uuids left, so stict defaults to false.
 *
 * @param  [type]  $uuid                 string to verify
 * @param  boolean $nullok               accept null value or null key as valid (default false)
 * @param  boolean $strict               apply strict UUID v4 implentation (default false)
 * @return boolean
 */
function opensim_isuuid( $uuid, $nullok = false, $strict = false ) {
	if ( $uuid == null ) {
		return $nullok;
	}
	if ( defined( 'NULL_KEY' ) && $uuid == NULL_KEY ) {
		return $nullok;
	}

	if ( $strict ) {
		return ( preg_match( '/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $uuid ) );
	} else {
		return ( preg_match( '/^[0-9A-F]{8,8}-[0-9A-F]{4,4}-[0-9A-F]{4,4}-[0-9A-F]{4,4}-[0-9A-F]{12,12}$/i', $uuid ) );
	}
}

/**
 * Sanitize a destination URI or URL
 *
 * @param  string  $url                             url or uri (secondlife:// url, hop:// url, region name...)
 * @param  string  $gatekeeperURL           default login uri to add to urls sithout host:port
 * @param  boolean $array_outout            output as array
 * @return string       (default)                   $host:$port $region/$pos
 *           or array                                           array($host, $port, $region, $pos)
 */
function opensim_sanitize_uri( $url, $gatekeeperURL = null, $array_outout = false ) {
	// $normalized = opensim_format_tp($uri, TPLINK_TXT);
	$host   = null;
	$port   = null;
	$region = null;
	$pos    = null;
	$uri    = urldecode( trim( $url ) );
	$uri    = preg_replace( '#^(.*://)?(([A-Za-z0-9_-]+\.[A-Za-z0-9\._-]+)([:/ ]+)?)?(([0-9]+)([ /:]))?([^/]+)(/|$)(.*)#', '$3:$6:$8/$10', "$uri" );
	$uri    = preg_replace( '/^([^:]+)::([0-9]+)/', '$1:$2', $uri );
	$uri    = preg_replace( '+[:/]*$+', '', $uri );
	$split  = explode( '/', $uri );
	$uri    = array_shift( $split );
	if ( count( $split ) == 2 || count( $split ) == 3 ) {
		$pos = implode( '/', $split );
	} else {
		$pos = '';
	}
	// $pos = preg_replace('+[^0-9/]+e', '', $pos);
	$split = explode( ':', $uri );

	if ( count( $split ) == 1 ) {
		$region = $split[0];
	} elseif ( count( $split ) == 2 && preg_match( '/ /', $split[1] ) ) {
		// could probably improve the preg_replace to avoid this
		$host   = $split[0];
		$split  = explode( ' ', $split[1] );
		$port   = $split[0];
		$region = $split[1];
	} elseif ( preg_match( '/[a-z].*\.[a-z]/', $split[0] ) ) {
			$host = array_shift( $split );
		if ( preg_match( '/^[0-9]+$/', $split[0] ) ) {
			$port = array_shift( $split );
		} else {
			$port = 8002;
		}
			$region = preg_replace( ':^/*:', '', @$split[0] );
	} elseif ( function_exists( 'w4os_grid_login_uri' ) ) {
		$host = parse_url( w4os_grid_login_uri(), PHP_URL_HOST );
		$port = parse_url( w4os_grid_login_uri(), PHP_URL_HOST );
		if ( preg_match( '/^[0-9]+$/', $split[0] ) ) {
			array_shift( $split );
		}
		$region = preg_replace( ':^/*:', '', @$split[0] );
	} else {
		if ( empty( $gatekeeperURL ) ) {
			return false;
		}
		$region = $split[2];
		$split  = explode( ':', preg_replace( '#.*://([^/]+)/?.*#', '$1', $gatekeeperURL ) );
		$host   = $split[0];
		$port   = $split[1];
	}
	if ( empty( $host ) & ! empty( $gatekeeperURL ) ) {
		$split = explode( ':', preg_replace( '#.*://([^/]+)/?.*#', '$1', $gatekeeperURL ) );
		$host  = $split[0];
		$port  = $split[1];
	}
	if ( empty( $port ) & ! empty( $host ) ) {
		$port = 80;
	}
	$host   = strtolower( trim( $host ) );
	$region = trim( str_replace( '_', ' ', $region ) );
	if ( is_numeric( $region ) ) {
		$pos    = "$region/$pos";
		$region = '';
	}
	if ( $array_outout ) {
		return array(
			'host'       => $host,
			'port'       => $port,
			'region'     => $region,
			'pos'        => $pos,
			'gatekeeper' => "http://$host:$port",
			'key'        => strtolower( "$host:$port/$region" ),
		);
	} else {
		return trim(
			$host
			. ( empty( $port ) ? '' : ":$port" )
			. ( empty( $region ) ? '' : " $region" )
			. ( empty( $pos ) ? '' : "/$pos" ),
			":/ \n\r\t\v\x00"
		);
	}

	// trim(string $string, string $characters = " \n\r\t\v\x00"): string
	// return preg_replace('#^[: ]*(.*)/*$#', '$1', "$host:$port $region" . ((empty($pos)) ? '' : "/$pos"));
}

/**
 * Format destination uri as a valid local or hypergrid link url
 *
 * @param  string  $uri      Destination uri, as "host:port:Region Name" or already formatted URL
 * @param  integer $format  The desired format as binary flags. Several values can be specified with an addition
 *                          e.g. TPLINK_V3HG + TPLINK_APPTP
 *                          TPLINK_LOCAL or 1:   secondlife://Region Name/x/y/z
 *                          TPLINK_HG or 2:      original HG format (obsolete?)
 *                          TPLINK_V3HG or 4:    v3 HG format (Singularity)
 *                          TPLINK_HOP or 8:     hop:// format (FireStorm)
 *                          TPLINK_TXT or 16:    host:port Region Name
 *                          TPLINK_APPTP or 32:  secondlife:///app/teleport link
 *                          TPLINK_MAP or 64:    (not implemented)
 *                          127:                      output all formats
 * @param  string  $sep      Separator for multiple formats, default new line
 * @return string
 */
function opensim_format_tp( $uri, $format = TPLINK, $sep = "\n" ) {
	if ( empty( $uri ) ) {
		return;
	}
	$parts = parse_url( $uri );

	// $uri = preg_replace('#!#', '', $uri);
	// $uri = preg_replace('#.*://+#', '', $uri);
	// $uri = preg_replace('#[\|:]#', '/', $uri);
	// $uri = preg_replace('#^([^/]+)/([0-9]+)/#', '$1:$2/', $uri);
	// $uri = preg_replace('#^[[:blank:]]*#', '', $uri);
	// echo "$uri ";
	// // $uri = preg_replace('#(\d{4}):#', '$1/', $uri);
	// $parts = explode("/", $uri);
	// $loginuri = array_shift($parts);
	// $hostparts = explode(":", $loginuri);
	// $host = $hostparts[0];
	// $port = (empty($hostparts[1])) ? 80 : $hostparts[1];
	// $region = urldecode(array_shift($parts));
	// $pos="";
	// if(count($parts) >=3 && is_numeric($parts[0]) && is_numeric($parts[1]) && is_numeric($parts[2]) ) {
	// $posparts = array($parts[0],$parts[1],$parts[2]);
	// $pos = join('/', $posparts);
	// $pos_sl = ($parts[0]>=256 || $parts[0]>=256) ? "" : $pos;
	// }
	$uri_parts = opensim_sanitize_uri( $uri, '', true );
	extract( $uri_parts );

	$regionencoded = urlencode( $region );
	$region_hop    = str_replace( ' ', '%20', $region );
	$pos_mandatory = ( empty( $pos ) ) ? '128/128/25' : $pos;
	$links         = array();
	if ( $format & TPLINK_TXT ) {
		$links[ TPLINK_TXT ] = "$host:$port $region/$pos";
	}
	if ( $format & TPLINK_LOCAL || ( $format & TPLINK_HG && empty( $host ) ) ) {
															$links[ TPLINK_LOCAL ] = "secondlife://$region/$pos";
	}
	if ( $format & TPLINK_HG ) {
		$links[ TPLINK_HG ] = "secondlife://$host:$port $region/$pos";
	}
	if ( $format & TPLINK_V3HG ) {
		$links[ TPLINK_V3HG ] = "secondlife://http|!!$host|$port+$region";
	}
	if ( $format & TPLINK_HOP ) {
		$links[ TPLINK_HOP ] = "hop://$host:$port/$region_hop" . ( empty( $pos ) ? '' : "/$pos" );
	}
	if ( $format & TPLINK_APPTP ) {
		$links[ TPLINK_APPTP ] = "secondlife:///app/teleport/$host:$port+$regionencoded/" . ( ( ! empty( $pos_sl ) ) ? "$pos_sl/" : '' );
	}
	// if ($format & TPLINK_MAP)        $links[TPLINK_MAP]      = "secondlife:///app/map/$host:$port+$regionencoded/$pos";
	$links = preg_replace( '#^[^[:alnum:]]*|[^[:alnum:]]+$#', '', $links );

	return join( $sep, $links );
}

/**
 * Use xmlrpc link_region method to request link_region data from robust
 *
 * @param  mixed  $args   region uri or sanitized region array
 * @param  string $var      output a single variable value
 * @return array (or string if var specified)
 */
function opensim_link_region( $args, $var = null ) {
	if ( empty( $args ) ) {
		return array();
	}
	global $OSSEARCH_CACHE;

	if ( is_array( $args ) ) {
		$region_array = $args;
	} else {
		$region_array = opensim_sanitize_uri( $args, '', true );
	}
	extract( $region_array ); // $host, $port, $region, $pos, $gatekeeper, $key

	if ( isset( $OSSEARCH_CACHE['link_region'][ $key ] ) ) {
		$link_region = $OSSEARCH_CACHE['link_region'][ $key ];
	} else {
		$link_region                           = oxXmlRequest( $gatekeeper, 'link_region', array( 'region_name' => "$region" ) );
		$OSSEARCH_CACHE['link_region'][ $key ] = $link_region;
	}

	if ( $link_region ) {
		if ( $var ) {
			return $link_region[ $var ];
		} else {
			return $link_region;
		}
	}

	return array();
}

/**
 * Build region URL from array
 *
 * @param  array $region sanitized region array
 * @return string
 */
function opensim_region_url( $region ) {
	if ( ! is_array( $region ) ) {
		return false;
	}
	return $region['gatekeeper'] . ( empty( $region['region'] ) ? '' : ':' . $region['region'] ) . ( empty( $region['pos'] ) ? '' : '/' . $region['pos'] );
}

function opensim_get_region( $region_uri, $var = null ) {
	if ( empty( $region_uri ) ) {
		return array();
	}
	global $OSSEARCH_CACHE;
	$region = opensim_sanitize_uri( $region_uri, '', true );

	$gatekeeper = $region['gatekeeper'];

	$link_region = opensim_link_region( $region );

	$uuid = @$link_region['uuid'];
	if ( ! opensim_isuuid( $uuid ) ) {
		// error_log( "opensim_get_region $region_uri invalid uuid $uuid" );
		return array();
	}

	if ( isset( $OSSEARCH_CACHE['get_region'][ $uuid ] ) ) {
		$get_region = $OSSEARCH_CACHE['get_region'][ $uuid ];
	} else {
		$get_region = oxXmlRequest( $gatekeeper, 'get_region', array( 'region_uuid' => "$uuid" ) );
		// $get_region = oxXmlRequest('http://dev.w4os.org:8402/grid', 'get_region_by_name', ['scopeid' => NULL_KEY,'name'=>"$region"]);
		$OSSEARCH_CACHE['get_region'][ $uuid ] = $get_region;
	}
	$get_region['link_region'] = $link_region;

	if ( $get_region ) {
		if ( $var ) {
			return $get_region[ $var ];
		} else {
			return $get_region;
		}
	}
	return array();
}

/**
 * Check if region is online
 *
 * @param  mixed $region   region uri or sanitized region array
 * @return boolean                  true if online
 */
function opensim_region_is_online( $region ) {
	$data = opensim_link_region( $region );
	return ( $data && $data['result'] == 'True' );
}

function opensim_user_alert( $agentID, $message, $secureID = null ) {
	$agentServer = opensim_get_server_info( $agentID );
	if ( ! $agentServer ) {
		return false;
	}
	$serverip  = $agentServer['serverIP'];
	$httpport  = $agentServer['serverHttpPort'];
	$serveruri = $agentServer['serverURI'];

	$avatarSession = opensim_get_avatar_session( $agentID );
	if ( ! $avatarSession ) {
		return false;
	}
	$sessionID = $avatarSession['sessionID'];
	if ( $secureID == null ) {
		$secureID = $avatarSession['secureID'];
	}

	$request  = xmlrpc_encode_request(
		'UserAlert',
		array(
			array(
				'clientUUID'            => $agentID,
				'clientSessionID'       => $sessionID,
				'clientSecureSessionID' => $secureID,
				'Description'           => $message,
			),
		)
	);
	$response = currency_xmlrpc_call( $serverip, $httpport, $serveruti, $request );

	return $response;
}

/**
 * [oxXmlRequest description]
 *
 * @param  string $gatekeeper               [description]
 * @param  string $method                   [description]
 * @param  array  $request                  [description]
 * @return array             received xml response
 */
function oxXmlRequest( $gatekeeper, $method, $request ) {
	$xml_request = xmlrpc_encode_request( $method, array( $request ) );

	$context = stream_context_create(
		array(
			'http' => array(
				'method'  => 'POST',
				'header'  => 'Content-Type: text/xml' . "\r\n",
				'timeout' => 3, // most of the time below 1 sec, but leave some time for slow ones
				'content' => $xml_request,
			),
		)
	);

	$response = @file_get_contents( $gatekeeper, false, $context );
	if ( $response === false ) {
		return false;
	}

	$xml_array = xmlrpc_decode( $response );
	if ( empty( $xml_array ) ) {
		return;
	}
	if ( is_array( $xml_array ) & ! xmlrpc_is_fault( $xml_array ) ) {
		return $xml_array;
	}

	return false;
}

function osXmlResponse( $success = true, $errorMessage = false, $data = false ) {
	global $request_key;

	// Data given, output as xmlrpc
	if ( is_array( $data ) ) {
		if( ! $success && ! empty( $errorMessage ) ) {
			# Avoid duplicate error messages
			# TODO: improve to make sure we don't cache error messages for different requests
			#	(currently $request_key only identifies search arguments, client IP and gateway url,
			# 	but client IP is the simulator, not the actual user, so in theory, the key could 
			# 	be identical for two simultaneous requests by different users in the same region, 
			#	although this is unlikely to happen)

			$tmp_dir = get_writable_tmp_dir();
			$cache = $tmp_dir . '/cache-' . $request_key;
			# Check if file exists and is not older than 5 seconds
			if ( file_exists( $cache ) && ( time() - filemtime( $cache ) < 1.5 ) ) {
				$errorMessage = '';
			} else {
				file_put_contents( $cache, $errorMessage );
			}
		}

		$array = array(
			'success'      => $success,
			'errorMessage' => $errorMessage,
			'data'		 => $data,
		);

		$response_xml = xmlrpc_encode( $array );
		
		echo $response_xml;
		return;
	}

	// No data given, output simple boolean or error message, no change here
	if ( $success ) {
		$answer = new SimpleXMLElement( '<boolean>true</boolean>' );
	} else {
		$answer = new SimpleXMLElement( "<error>$errorMessage</error>" );
	}
	echo $answer->asXML();
}

function osXmlDie( $message = '' ) {
	osXmlResponse( false, $message, array() );
	die;
}

function osNotice( $message ) {
	echo $message . "\n";
}

function osAdminNotice( $message, $error_code = 0, $die = false ) {
	// get calling function and file
	$trace = debug_backtrace();

	if ( isset( $trace[1] ) ) {
		$caller = $trace[1];
	} else {
		$caller = $trace[0];
	}
	$file     = empty( $caller['file'] ) ? '' : $caller['file'];
	$function = $caller['function'] . '()' ?? 'main';
	$line     = $caller['line'] ?? 0;
	$class    = $caller['class'] ?? 'main';
	$type     = $caller['type'] ?? '::';
	if ( $class != 'main' ) {
		$function = $class . $type . $function;
	}
	$file    = $file . ':' . $line;
	$message = sprintf(
		'%s%s: %s in %s',
		$function,
		empty( $error_code ) ? '' : " Error $error_code",
		$message,
		$file,
	);
	error_log( $message );
	if ( $die == true ) {
		die( $error_code );
	}
}

/**
 * Flush output and free client so following commands are executed in background
 *
 * @return void
 */
function dontWait() {
	$size = ob_get_length();

	header( "Content-Length:$size" );
	header( 'Connection:close' );
	header( 'Content-Encoding: none' );
	header( 'Content-Type: text/html; charset=utf-8' );

	ob_flush();
	ob_end_flush();
	flush();
}

if ( ! function_exists( 'osdebug' ) ) {
	function osdebug( $message = '' ) {
		if ( empty( $message ) ) {
			return;
		}
		if ( ! is_string( $message ) ) {
			$message = print_r( $message, true );
		}
		error_log( 'osdebug ' . $message );
		echo $message . "\n";
	}
}

function set_helpers_locale( $locale = null, $domain = 'messages' ) {
	mb_internal_encoding( 'UTF-8' );
	$encoding = mb_internal_encoding();

	if ( isset( $_GET['l'] ) ) {
		$locale = $_GET['l'];
	}
	$languages = array_filter( array_merge( array( $locale ), explode( ',', $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) );

	// $results = putenv("LC_ALL=$locale");
	// if (!$results) {
	// exit ('putenv failed');
	// }

	// $currentLocale = setlocale(LC_ALL, 0);
	$user_locales = array_unique( array( $locale, $locale . ".$encoding", $locale . '.UTF-8', $locale . '.utf8', $locale, 0 ) );

	$user_locales = array_map(
		function ( $code ) {
			return preg_replace(
				array( '/;.*/', '/-/' ),
				array( '', '_' ),
				$code
			);
		},
		$languages
	);

	// Generate variants with different encodings appended
	$variants = array();
	foreach ( $user_locales as $lang ) {
		$variants[] = $lang;
		$variants[] = "$lang.$encoding";
		// $variants[] = "$lang.UTF-8";
	}

	$variants = array_unique( $variants );
	if ( ! setlocale( LC_ALL, $variants ) ) {
		// error_log( "setlocale() failed: none of  '" . join( ', ', $variants ) . "' does exist in this environment or setlocale() is not available on this platform" );
		setlocale( LC_ALL, 0 );
		return 0;
	}

	bindtextdomain( $domain, './locales' );
	textdomain( $domain );
}

function get_writable_tmp_dir() {
	if(isset($_GLOBALS['tmp_dir'])) {
		return $_GLOBALS['tmp_dir'];
	}
	$dirs = array( sys_get_temp_dir(), ini_get('upload_tmp_dir'), '/tmp', '/var/tmp', '/usr/tmp', '.' );
	foreach ( $dirs as $dir ) {
		if ( @is_writable( $dir ) ) {
			$_GLOBALS['tmp_dir'] = $dir;
			return $dir;
		}
	}
	error_log( __FILE__ . ':' . __LINE__ . ' ERROR - could not find a writable temporary directory, check web server and PHP config' );
	return false;
	// return '/tmp';
}

if( ! defined( 'NULL_KEY') ) define( 'NULL_KEY', '00000000-0000-0000-0000-000000000000' );
if( ! defined( 'TPLINK_LOCAL') ) {
	define( 'TPLINK_LOCAL', 1 ); // seconlife://Region/x/y/z
	define( 'TPLINK_HG', 2 ); // seconlife://yourgrid.org:8002 Region/x/y/z
	define( 'TPLINK_V3HG', 4 ); // the overcomplicated stuff!
	define( 'TPLINK_HOP', 8 ); // hop://yourgrid.org:8002:Region/x/y/z
	define( 'TPLINK_TXT', 16 ); // yourgrid.org:8002:Region/x/y/z
	define( 'TPLINK_APPTP', 32 ); // secondlife:///app/teleport/yourgrid.org:8002:Region/x/y/z
	define( 'TPLINK_MAP', 64 ); // secondlife:///app/map/yourgrid.org:8002:Region/x/y/z
	define( 'TPLINK', pow( 2, 8 ) - 1 ); // all formats
	define( 'TPLINK_DEFAULT', TPLINK_HOP ); // default
}

define( 'HELPERS_LOCALE_DIR', dirname( __DIR__ ) . '/languages' );

function os_cache_get( $key, $default = null ) {
	global $oshelpers_cache;
	return isset( $oshelpers_cache[$key] ) ? $oshelpers_cache[$key] : $default;
}

function os_cache_set( $key, $value, $expire = 0 ) {
	global $oshelpers_cache;
	$oshelpers_cache[$key] = $value;
}

/**
 * OpenSim source to help further attempts to allow Hypergrid search results.
 * Infouuid is a fake parcelid resolving to region handle and (region-level?)
 * pos which might (or not) give enough information to allow hg results.
 * 1. Link region locally with link-region (or directly in db?)
 * 2. Use local link region handle (instead of remote one) to generate infouuid
 * 3. Use local link Global pos instead of remote one
 */
//
// public static UUID BuildFakeParcelID(ulong regionHandle, uint x, uint y)
// {
// byte[] bytes =
// {
// (byte)regionHandle, (byte)(regionHandle >> 8), (byte)(regionHandle >> 16), (byte)(regionHandle >> 24),
// (byte)(regionHandle >> 32), (byte)(regionHandle >> 40), (byte)(regionHandle >> 48), (byte)(regionHandle >> 56),
// (byte)x, (byte)(x >> 8), 0, 0,
// (byte)y, (byte)(y >> 8), 0, 0 };
// return new UUID(bytes, 0);
// }
//
// public static UUID BuildFakeParcelID(ulong regionHandle, uint x, uint y, uint z)
// {
// byte[] bytes =
// {
// (byte)regionHandle, (byte)(regionHandle >> 8), (byte)(regionHandle >> 16), (byte)(regionHandle >> 24),
// (byte)(regionHandle >> 32), (byte)(regionHandle >> 40), (byte)(regionHandle >> 48), (byte)(regionHandle >> 56),
// (byte)x, (byte)(x >> 8), (byte)z, (byte)(z >> 8),
// (byte)y, (byte)(y >> 8), 0, 0 };
// return new UUID(bytes, 0);
// }
