<?php
/**
 * @brief		DataLayer Handler class
 * @author		<a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
 * @copyright	(c) Invision Power Services, Inc.
 * @license		https://www.invisioncommunity.com/legal/standards/
 * @package		Invision Community
 * @since		Feb 2022
 */

namespace IPS\core\DataLayer;

/* To prevent PHP errors (extending class does not exist) revealing path */

use IPS\Data\Store;
use IPS\Settings;
use OutOfRangeException;
use UnexpectedValueException;
use function defined;
use function is_array;
use function preg_replace;
use function str_replace;

if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Data Layer Handler class
 */
#[\AllowDynamicProperties]
class Handler
{
	public static string $cicHandlerClass = '\IPS\cloud\DataLayer\Handler';

	public static string $handlerCacheKey = 'dataLayerHandlers';

	public string $id = 'gtm';

	/**
	 * Returns the enabled handlers to be used in the global template as an array of head and body HTML snippets.
	 *
	 * @return array
	 */
	public static function loadForTemplates() : array
	{
		$return = array();

		/* try to get from cache */
		$key = static::$handlerCacheKey;
		try
		{
			$cached = json_decode( Store::i()->$key, true );
			if ( is_array( $cached ) )
			{
				return $cached;
			}
		}
		catch ( OutOfRangeException $e ) {}

		$handlers = static::loadEnabled();
		if ( Settings::i()->core_datalayer_use_gtm )
		{
			$handlers[] = static::gtm();
		}

		$headInserts    = "<!-- Handlers -->\n\n<!-- Initializers -->\n";
		$bodyInserts    = "<!-- Handlers -->\n";
		$headCodes      = "";
		$eventCallbacks = array();
		$propertiesCallbacks = array();

		foreach ( $handlers as $handler )
		{

			if ( $handler->head_code )
			{
				$headCodes .= "\n{$handler->head_code}\n";
			}

			if ( $handler->body_code )
			{
				$bodyInserts .= "\n{$handler->body_code}\n";
			}

			if ( $handler->initializer_code AND $handler->use_js )
			{
				$formatted  = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->initializer_code );
				$code       = <<<HTML
<script>
	let initcode{$handler->id} = context => {
        if ( !(context instanceof Object) ) {
            return;
        }
        
        /* Set the key and time */
        let ips_time = IpsDataLayerConfig._properties.ips_time.enabled ? IpsDataLayerConfig._properties.ips_time.formatted_name : false;
        let ips_key  = IpsDataLayerConfig._properties.ips_key.enabled ? IpsDataLayerConfig._properties.ips_key.formatted_name : false;
        
        if ( ips_time ) {
            context[ips_time] = Math.floor( Date.now() / 1000 );
        }
        
        if ( ips_key ) {
            let s = i => {
                return Math.floor((1 + Math.random()) * Math.pow(16, i))
                    .toString(16)
                    .substring(1);
            };

            let mt = Date.now();
            let sec = Math.floor(mt / 1000);
            let secString = sec.toString(16);
            secString = secString.substring( secString.length - 8 );
            let ms  = ( mt - ( sec * 1000 ) ) * 1000; // milliseconds
            let msString = (ms + 0x100000).toString(16).substring(1);
            let randomId = secString + msString + s(1) + '.' + s(4) + s(4);
            context[ips_key] = randomId;
        }
        
        for ( let i in context ) {
            if ( context[i] === null ) {
                context[i] = undefined;
            }
        }
        
        try {
			$formatted
        } catch (e) {
            Debug.error('Bad Data Layer Initializer: Event initializer failed!');
        }
    };
    initcode{$handler->id}(IpsDataLayerContext || {});
</script>
HTML;
				$code           = preg_replace( '/\/\/(.*)[\n|\r]/', '/*$1*/', $code );
				$code           = preg_replace( '/\s+/', ' ', $code);
				$headInserts    .= str_replace("\n", ' ', $code ) . "\n";
			}

			if ( $handler->event_handler AND $handler->use_js )
			{
				$formatted  = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->event_handler );
				$code       = <<<JS
( () => _event => {
    try {
		$formatted
    } catch (e) {
        Debug.error( e );
    }
} )
JS;
				$code           = preg_replace( '/\/\/(.*)[\n|\r]?/', '/*$1*/', $code );
				$code               = preg_replace( '/\s+/', ' ', $code);
				$eventCallbacks[]   = str_replace( "\n", ' ', $code );
			}

			if ( $handler->properties_handler AND $handler->use_js )
			{
				$formatted  = str_replace( '{dataLayer}', $handler->datalayer_key, $handler->properties_handler );
				$code       = <<<JS
( () => _properties => {
    try {
		$formatted
    } catch (e) {
        Debug.error( e );
    }
} )
JS;
				$code           = preg_replace( '/\/\/(.*)[\n|\r]?/', '/*$1*/', $code );
				$code                   = preg_replace( '/\s+/', ' ', $code);
				$propertiesCallbacks[]  = str_replace( "\n", ' ', $code );
			}
		}

		$headInserts    .= "<!-- END Initializers -->\n";
		$headInserts    .= "\n<!-- Head Snippets -->\n$headCodes\n<!-- END Head Snippets -->\n";
		$eventsJoined   = implode( ",\n\t", $eventCallbacks );
		$headInserts    .= <<<HTML

<!-- Event Callbacks -->
<script>
const IpsDataLayerEventHandlers = [
    $eventsJoined
]
</script>
<!-- END Event Callbacks -->

HTML;

		$propertiesJoined   = implode( ",\n\t", $propertiesCallbacks );
		$headInserts        .= <<<HTML

<!-- Properties Callbacks -->
<script>
const IpsDataLayerPropertiesHandlers = [
    $propertiesJoined
]
</script>
<!-- END Properties Callbacks -->

HTML;

		$headInserts .= "\n<!-- END Handlers -->";
		$bodyInserts .= "\n<!-- END Handlers -->";

		$return = array(
			'headInserts' => $headInserts,
			'bodyInserts' => $bodyInserts,
		);

		/* add to cache before returning */
		Store::i()->$key = json_encode( $return );

		return $return;
	}

	/**
	 * Get all enabled handlers
	 *
	 * @return static[]
	 */
	public static function loadEnabled() : array
	{
		if ( class_exists( static::$cicHandlerClass ) )
		{
			$class = static::$cicHandlerClass;
			return $class::loadEnabled();
		}
		return array();
	}

	/**
	 * Get a built in gtm handler
	 *
	 * @return static
	 */
	public static function gtm() : static
	{
		$gtm = new static();
		$gtm->initializer_code = <<<JS

if (context instanceof Object) {
	{dataLayer} = {dataLayer} || [];
	{dataLayer}.push(context);
    return;
}
Debug.log( 'Invalid Data Layer Context: The IPS GTM Data Layer Initializer failed because the context wasn\'t an Object' );

JS;
		$gtm->head_code = Settings::i()->googletag_head_code;
		$gtm->use_js    = true;
		$gtm->body_code = Settings::i()->googletag_noscript_code;
		$gtm->datalayer_key = Settings::i()->core_datalayer_gtmkey ?: 'window.dataLayer';
		$gtm->event_handler = <<<JS

if ( (_event._properties instanceof Object) && (typeof _event._key === 'string')) {
	{dataLayer} = {dataLayer} || [];
    let properties = {};
    for ( let pKey in _event._properties ) {
        properties[_event._key + '.' + pKey] = _event._properties[pKey];
    }
	{dataLayer}.push( { ...properties, 'event': _event._key } );
    return;
}
Debug.log( 'Invalid Data Layer Event: An event wasn\'t processed by the IPS GTM Data Layer Handler. The event\'s _key has to be a string, and its _properties has to be an Object.' );

JS;
		$gtm->properties_handler = <<<JS

if ( _properties instanceof Object ) {
    delete _properties.event; // this cannot be set since this handler is NOT for adding GTM events
    {dataLayer} = {dataLayer} || [];
    {dataLayer}.push( _properties );
}

JS;
		return $gtm;
	}

	/**
	 * Get all handler objects according to the where clause
	 *
	 * @param   array   $where   The where clause
	 *
	 * @return  array
	 * @throws UnexpectedValueException If $where contains invalid fields and/or invalid SQL logic
	 */
	public static function loadWhere( array $where=array() ) : array
	{
		if ( ( $class = static::$cicHandlerClass ) AND class_exists( $class ) )
		{
			return $class::loadWhere( $where );
		}
		return array();
	}

	/**
	 * Mock instance method since our autoloader will complain if there are only static methods in a class
	 *
	 * @return bool
	 */
	public function mockFunction() : bool
	{
		return true;
	}
}