<?php 

/**
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            https://www.tassos.gr
 * @copyright       Copyright © 2024 Tassos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace NRFramework;

defined('_JEXEC') or die;

use Joomla\Registry\Registry;
use \NRFramework\Cache;
use Joomla\CMS\Factory;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Uri\Uri;
use Joomla\Filesystem\File;
use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Helper\ModuleHelper;

class Functions
{
    /**
     * Add HTML before the closing </body> tag.
     *
     * @param   string  $html   The HTML to prepend to </body>
     * @param   boolean $once   If true, the HTML will be added only once.
     * 
     * @return  void
     */
    public static function appendToBody($html, $once = false)
    {
        if ($once)
        {
            $hash = md5($html);

            if (Cache::has($hash))
            {
                return;
            }

            Cache::set($hash, true);
        }

        $app = Factory::getApplication();

        $app->registerEvent('onAfterRender', function() use ($app, $html)
        {
            $buffer = $app->getBody();

            $closingTag = '</body>';
    
            if (strpos($buffer, $closingTag))
            {
                // If </body> exists prepend the given HTML
                $buffer = str_replace($closingTag, $html . $closingTag, $buffer);
            } else 
            {
                // If </body> does not exist append to document's end
                $buffer .= $html;
            }
            
            $app->setBody($buffer);
        });
    }

    /**
     * Fix arrays, remove duplicate items, null items and whitespace around item values.
     *
     * @param  array $subject
     * 
     * @return array    The new cleaned array
     */
    public static function cleanArray($subject)
    {
        if (!is_array($subject))
        {
            return $subject;
        }

        $subject = array_map(function($str)
        {
            return is_null($str) ? '' : trim($str);
        }, $subject);

        $subject = array_unique($subject);

        // Remove empty items. We use a custom callback here because the default behavior of array_filter removes 0 values as well.
        $subject = array_filter($subject, function($value)
        {
            return ($value !== null && $value !== false && $value !== ''); 
        });

        return $subject;
    }

    /**
     *  Attempt to convert a subject to array
     *
     *  @param  mixed  $subject
     * 
     *  @return array
     */
    public static function makeArray($subject)
    {
        if (empty($subject))
        {
            return [];
        }

        if (is_object($subject))
        {
            return (array) $subject;
        }

        if (!is_array($subject))
        {
            // replace newlines with commas
            $subject = str_replace(PHP_EOL, ',', $subject);
    
            // split keywords on commas
            $subject = explode(',', $subject);
        }

        // Now that we have an array, run some housekeeping.
        $arr = $subject;

        $arr = self::cleanArray($arr);

        // Reset keys
        $arr = array_values($arr);

        return $arr;
    }

    /**
     * Return the real site base URL by ignoring the live_site configuration option.
     *
     * @param  bool $ignore_admin   If enabled and we are browsing administrator, we will get the front-end site root URL.
     *
     * @return string
     */
    public static function getRootURL($ignore_admin = true) 
    {
        $factory = Factory::getConfig();

        // Store the original live_site value
        $live_site_original = $factory->get('live_site', '');

        // If we live_site is not set, do not proceed further. Return the default website base URL.
        if (empty($live_site_original))
        {
            return $ignore_admin ? Uri::root() : Uri::base();
        }

        // Remove the live site
        $factory->set('live_site', '');

        // Remove all cached Uri instances
        Uri::reset();

        // Get a new URL. The live_site option should be ignored.
        $base_url = $ignore_admin ? Uri::root() : Uri::base();

        // Set back the original live_site
        $factory->set('live_site', $live_site_original);
        Uri::reset();

        return $base_url;
    }

    /**
     * Insert an associative array into a specific position in an array
     *
     * @param $original array 	The original array to add to
     * @param $new array 		The new array of values to insert into the original
     * @param $offset int 		The position in the array ( 0 index ) where the new array should go
     *
     * @return array 		The new combined array
     */
    public static function array_splice_assoc($original,$new,$offset)
    {
        return array_slice($original, 0, $offset, true) + $new + array_slice($original, $offset, NULL, true);  
    }

    public static function renderField($fieldname)
    {
        $fieldname = strtolower($fieldname);

		require_once JPATH_PLUGINS . '/system/nrframework/fields/' . $fieldname . '.php';

        $classname = '\JFormField' . $fieldname;

		$field = new $classname();

        $element = new \SimpleXMLElement('
            <field name="' . $classname . '" type="' . $classname . '"

			/>');
			
        $field->setup($element, null);
        
        return $field->__get('input');
    }

	/**
	 *  Checks if an array of values (needle) exists in a text (haystack)
	 *
	 *  @param   array   $needle            The searched array of values.
	 *  @param   string  $haystack          The text
	 *  @param   bool    $case_insensitive  Indicates whether the letter case plays any role
	 *
	 *  @return  bool
	 */
	public static function strpos_arr($needles, $haystack, $case_insensitive = false)
	{
        $needles = !is_array($needles) ? (array) $needles : $needles;
        $haystack = $case_insensitive ? strtolower($haystack) : $haystack;

		foreach ($needles as $needle)
		{
            $needle = $case_insensitive ? strtolower($needle) : $needle;

			if (strpos($haystack, $needle) !== false) 
			{
				// stop on first true result
				return true; 
			}
		}

		return false;
	}

    /**
     *  Log message to framework's log file
     *
     *  @param   mixed  $data    Log message
     *
     *  @return  void
     * 
     *  @deprecated Stop using method
     */
    public static function log($data)
    {
    }

    /**
     *  Return's a URL with the Google Analytics Campaign Parameters appended to the end
     *
     *  @param   string  $url       The URL
     *  @param   string  $medium    Campaign Medium
     *  @param   string  $campaign  Campaign Name
     *
     *  @return  string
     */
    public static function getUTMURL($url, $medium = 'upgradebutton', $campaign = 'freeversion')
    {
        if (!$url)
        {
            return;
        }

        $utm  = 'utm_source=CustomerBackend&utm_medium=' . $medium . '&utm_campaign=' . $campaign;
        $char = strpos($url, '?') === false ? '?' : '&';

        return $url . $char . $utm;
    }

    /**
     *  Returns user's Download Key
     *
     *  @return  string
     */
    public static function getDownloadKey()
    {
        $class = new Updatesites();
        return $class->getDownloadKey();
    }

    /**
     *  Adds a script or a stylesheet to the document
     *
     *  @param  Mixed    $files           The files to be to added to the document
     *  @param  boolean  $appendVersion   Adds file versioning based on extension's version
     *
     *  @return void
     */
    public static function addMedia($files, $extension = "plg_system_nrframework", $appendVersion = true)
    {
        $doc       = Factory::getDocument();
        $version   = self::getExtensionVersion($extension);
        $mediaPath = Uri::root(true) . "/media/" . $extension;

        if (!is_array($files))
        {
            $files = array($files);
        }

        foreach ($files as $key => $file)
        {
            $fileExt  = File::getExt($file);
            $filename = $mediaPath . "/" . $fileExt . "/" . $file;
            $filename = ($appendVersion) ? $filename . "?v=" . $version : $filename;

            if ($fileExt == "js")
            {
                $doc->addScript($filename);
            }

            if ($fileExt == "css")
            {
                $doc->addStylesheet($filename);
            }
        }
    }

    /**
     *  Get the Framework version
     *
     *  @return  string  The framework version
     */
    public static function getVersion()
    {
        return self::getExtensionVersion("plg_system_nrframework");
    }

    /**
     *  Checks if document is a feed document (xml, rss, atom)
     *
     *  @return  boolean
     */
    public static function isFeed()
    {
        return (
            Factory::getDocument()->getType() == 'feed'
            || Factory::getDocument()->getType() == 'xml'
            || Factory::getApplication()->input->getWord('format') == 'feed'
            || Factory::getApplication()->input->getWord('type') == 'rss'
            || Factory::getApplication()->input->getWord('type') == 'atom'
        );
    }

    public static function loadLanguage($extension = 'plg_system_nrframework', $basePath = '')
    {
        if ($basePath && Factory::getLanguage()->load($extension, $basePath))
        {
            return true;
        }

        $basePath = self::getExtensionPath($extension, $basePath, 'language');

        return Factory::getLanguage()->load($extension, $basePath);
    }

    /**
     *  Returns extension ID
     *
     *  @param   string  $extension  Extension name
     *
     *  @return  integer
     * 
     *  @deprecated Use \NRFramework\Extension::getID instead
     */
    public static function getExtensionID($extension, $folder = null)
    {
        $type = is_null($folder) ? 'component' : 'plugin';
        return \NRFramework\Extension::getID($extension, $type, $folder);
    }

    /**
     *  Checks if extension is installed
     *
     *  @param   string  $extension  The extension element name
     *  @param   string  $type       The extension's type 
     *  @param   string  $folder     Plugin folder     * 
     *
     *  @return  boolean             Returns true if extension is installed
     * 
     *  @deprecated Use \NRFramework\Extension::isInstalled instead
     */
    public static function extensionInstalled($extension, $type = 'component', $folder = 'system')
    {
        return \NRFramework\Extension::isInstalled($extension, $type, $folder);
    }

    /**
     *  Returns the version number from the extension's xml file
     *
     *  @param   string  $extension  The extension element name
     *
     *  @return  string              Extension's version number
     */
    public static function getExtensionVersion($extension, $type = false)
    {
        $hash  = MD5($extension . "_" . ($type ? "1" : "0"));
        $cache = Cache::read($hash);

        if ($cache)
        {
            return $cache;
        }

        $xml = self::getExtensionXMLFile($extension);

        if (!$xml)
        {
            return false;
        }

        $xml = Installer::parseXMLInstallFile($xml);

        if (!$xml || !isset($xml['version']))
        {
            return '';
        }

        $version = $xml['version'];

        if ($type)
        {
            $extType = Extension::isPro($extension) ? 'Pro' : 'Free';
            $version = $xml["version"] . " " . $extType;
        }

        return Cache::set($hash, $version);
    }

    public static function getExtensionXMLFile($extension, $basePath = JPATH_ADMINISTRATOR)
    {
        $alias = explode("_", $extension);
        $alias = end($alias);

        $filename = (strpos($extension, 'mod_') === 0) ? "mod_" . $alias : $alias;
        $file = self::getExtensionPath($extension, $basePath) . "/" . $filename . ".xml";

        if (file_exists($file))
        {
            return $file;
        }
        
        return false;
    }

    /**
     * @deprecated // Use Extension::isPro();
     */
    public static function extensionHasProInstalled($extension)
    {
        return Extension::isPro($extension);
    }

    public static function getExtensionPath($extension = 'plg_system_nrframework', $basePath = JPATH_ADMINISTRATOR, $check_folder = '')
    {
        $path = '';

        switch (true)
        {
            case (strpos($extension, 'com_') === 0):
                $path = 'components/' . $extension;
                break;

            case (strpos($extension, 'mod_') === 0):
                $path = 'modules/' . $extension;
                break;

            case (strpos($extension, 'plg_system_') === 0):
                $path = 'plugins/system/' . substr($extension, strlen('plg_system_'));
                break;

            case (strpos($extension, 'plg_editors-xtd_') === 0):
                $path = 'plugins/editors-xtd/' . substr($extension, strlen('plg_editors-xtd_'));
                break;
        }

        if (empty($path))
        {
            return;
        }

        $check_folder = $check_folder ? '/' . $check_folder : '';
        $basePath = empty($basePath) ? JPATH_ADMINISTRATOR : $basePath;

        if (is_dir($basePath . '/' . $path . $check_folder))
        {
            return $basePath . '/' . $path;
        }

        if (is_dir(JPATH_ADMINISTRATOR . '/' . $path . $check_folder))
        {
            return JPATH_ADMINISTRATOR . '/' . $path;
        }

        if (is_dir(JPATH_SITE . '/' . $path . $check_folder))
        {
            return JPATH_SITE . '/' . $path;
        }

        return $basePath;
    }

    public static function renderModulePosition($position, $style = 'custom')
    {
        $modules = ModuleHelper::getModules($position);

        $attribs['style'] = $style;
        foreach ($modules as $module)
        {
            echo ModuleHelper::renderModule($module, $attribs);
        }
    }

    public static function loadModule($id, $moduleStyle = null)
    {  
        // Return if no module id passed
        if (!$id) 
        {
            return;
        }

        // Fetch module from db
        $db = Factory::getDBO();
        $query = $db->getQuery(true)
            ->select('*')
            ->from('#__modules')
            ->where('id='.$db->q($id));

        $db->setQuery($query);

        // Return if no modules found
        if (!$module = $db->loadObject()) 
        {
            return;
        }

        // Success! Return module's html
        return ModuleHelper::renderModule($module, $moduleStyle);
    }

    public static function fixDate(&$date)
    {
        if (!$date)
        {
            $date = null;

            return;
        }

        $date = trim($date);
        
        // Check if date has correct syntax: 00-00-00 00:00:00
        if (preg_match('#^[0-9]+-[0-9]+-[0-9]+( [0-9][0-9]:[0-9][0-9]:[0-9][0-9])$#', $date))
        {
            return;
        }
        
        // Check if date has syntax: 00-00-00 00:00
        // If so, add :00 (seconds)
        if (preg_match('#^[0-9]+-[0-9]+-[0-9]+ [0-9][0-9]:[0-9][0-9]$#', $date))
        {
            $date .= ':00';

            return;
        }

        // Check if date has a prepending date syntax: 00-00-00 ...
        // If so, add 00:00:00 (hours:mins;secs)
        if (preg_match('#^([0-9]+-[0-9]+-[0-9]+)#', $date, $match))
        {
            $date = $match[1] . ' 00:00:00';
            
            return;
        }

        // Date format is not correct, so return null
        // $date = null;
    }

    /**
     * Change date's timezone to UTC by modyfing the offset
     *
     * @param  string   $date   The date in timezone other than UTC
     * 
     * @return string   The date in UTC
     */
    public static function dateToUTC($date)
    {
		$date = is_string($date) ? trim($date) : $date;

		if (empty($date) || is_null($date) || $date == '0000-00-00 00:00:00')
		{
			return $date;
		}

        $timezone = Factory::getUser()->getParam('timezone', Factory::getConfig()->get('offset'));

        $date = new Date($date, $timezone);
        $date->setTimezone(new \DateTimeZone('UTC'));

        $dateUTC = $date->format('Y-m-d H:i:s', true, false);

        return $dateUTC;
    }

    /**
     * Applies the site's or the user's timezone to a given date.
     * 
     * @param   string  $date
     * @param   string  $format
     * 
     * @return  string
     */
    public static function applySiteTimezoneToDate($date, $format = 'Y-m-d H:i:s')
    {
        $timezone = new \DateTimeZone(Factory::getUser()->getParam('timezone', Factory::getConfig()->get('offset')));
        return Factory::getDate($date)->setTimezone($timezone)->format($format, true);
    }

    /**
     * Change date's timezone to UTC by modyfing the offset
     *
     * @param  string   $date   The date in timezone other than UTC
     * 
     * @return string   The date in UTC
     * 
     * @deprecated Use dateToUTC()
     */
    public static function fixDateOffset(&$date)
    {
        $date = self::dateToUTC($date);
    }

    // Text
    public static function clean($string) 
    {
        $string = str_replace(' ', '-', $string); // Replaces all spaces with hyphens.
        return preg_replace('/[^A-Za-z0-9\-]/', '', $string); // Removes special chars.
    }

    public static function dateTimeNow() 
    {
        return Factory::getDate()->format("Y-m-d H:i:s");
    }

    /**
     *  Get framework plugin's parameters
     *
     *  @return  Registry   The plugin parameters
     */
    public static function params()
    {
        $hash = md5('frameworkParams');

        if (Cache::has($hash))
        {
            return Cache::read($hash);
        }

        $db = Factory::getDBO();

        $result = $db->setQuery(
            $db->getQuery(true)
            ->select('params')
            ->from('#__extensions')
            ->where('element = ' . $db->quote('nrframework'))
        )->loadResult();

        return Cache::set($hash, new Registry($result));
    }

    /**
     * Checks whether string starts with substring.
     * 
     * @param   string  $string
     * @param   string  $query
     * 
     * @return  bool
     */
    public static function startsWith($string, $query)
    {
        return substr($string, 0, strlen($query)) === $query;
    }

    /**
     * Checks whether string end with substring.
     * 
     * @param   string  $string
     * @param   string  $substring
     * 
     * @return  bool
     */
    public static function endsWith($string, $substring)
    {
        $length = strlen($substring);

        return substr((string) $string, -$length) === $substring;
    }

    /**
     * Updates the Download Key in the framework plugin.
     * 
     * @param   string  $key
     * 
     * @return  bool
     */
    public static function updateDownloadKey($key)
    {
        if (empty($key))
        {
            return false;
        }

		// Update params
        $db = Factory::getDBO();

		// Get params
		$query = $db->getQuery(true)
            ->select($db->quoteName('params'))
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
            ->where($db->quoteName('element') . ' = ' . $db->quote('nrframework'));

        $db->setQuery($query);
        $params = $db->loadResult();

        $params = json_decode($params, true);
        
        // Set Download Key
        $params['key'] = $key;

        // Update params
		$query->clear()
            ->update('#__extensions')
            ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params)))
            ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
            ->where($db->quoteName('element') . ' = ' . $db->quote('nrframework'));
        $db->setQuery($query);
        $db->execute();

        return true;
    }

   	/**
	 * Return the current page's body regardless the system event fires the method.
	 *
	 * @return string
	 */
	public static function getBuffer()
	{
		$app = Factory::getApplication();

		// Try to get the whole body first. This will only work if the method is executed in the onAfterRender event.
		if ($body = $app->getBody())
		{
			return $body;
		}

		// We got an empty body. Probably, the method runs before the onAfterRender event. Let's give it another try.
		$buffer = $app->getDocument()->getBuffer();

		if (!is_array($buffer))
		{
			return;
		}

		$flatted = \Joomla\Utilities\ArrayHelper::flatten($buffer);

		return implode(' ', $flatted);
	}
}