Your IP : 216.73.216.240


Current Path : /home/juvelize/saulnois/libraries/ic_library/Vendor/Icalcreator/src/Xml/
Upload File :
Current File : /home/juvelize/saulnois/libraries/ic_library/Vendor/Icalcreator/src/Xml/Formatter.php

<?php
/**
 * iCalcreator, the PHP class package managing iCal (rfc2445/rfc5445) calendar information.
 *
 * This file is a part of iCalcreator.
 *
 * @author    Kjell-Inge Gustafsson, kigkonsult <ical@kigkonsult.se>
 * @copyright 2007-2024 Kjell-Inge Gustafsson, kigkonsult AB, All rights reserved
 * @link      https://kigkonsult.se
 * @license   Subject matter of licence is the software iCalcreator.
 *            The above copyright, link, package and version notices,
 *            this licence notice and the invariant [rfc5545] PRODID result use
 *            as implemented and invoked in iCalcreator shall be included in
 *            all copies or substantial portions of the iCalcreator.
 *
 *            iCalcreator is free software: you can redistribute it and/or modify
 *            it under the terms of the GNU Lesser General Public License as
 *            published by the Free Software Foundation, either version 3 of
 *            the License, or (at your option) any later version.
 *
 *            iCalcreator is distributed in the hope that it will be useful,
 *            but WITHOUT ANY WARRANTY; without even the implied warranty of
 *            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *            GNU Lesser General Public License for more details.
 *
 *            You should have received a copy of the GNU Lesser General Public License
 *            along with iCalcreator. If not, see <https://www.gnu.org/licenses/>.
 */
declare( strict_types = 1 );
namespace Kigkonsult\Icalcreator\Xml;

use DateInterval;
use DateTime;
use Exception;
use InvalidArgumentException;
use Kigkonsult\Icalcreator\CalendarComponent;
use Kigkonsult\Icalcreator\IcalInterface;
use Kigkonsult\Icalcreator\Pc;
use Kigkonsult\Icalcreator\Util\DateIntervalFactory;
use Kigkonsult\Icalcreator\Util\DateTimeFactory;
use Kigkonsult\Icalcreator\Util\GeoFactory;
use Kigkonsult\Icalcreator\Util\StringFactory;
use Kigkonsult\Icalcreator\Vcalendar;
use Kigkonsult\Icalcreator\Vevent;
use SimpleXMLElement;

use function array_change_key_case;
use function count;
use function htmlspecialchars;
use function implode;
use function in_array;
use function is_array;
use function sprintf;
use function strcasecmp;
use function strlen;
use function strstr;
use function strtolower;
use function strtoupper;
use function substr;

/**
 * iCalcreator XML (rfc6321) formatter class
 *
 * @since 2.41.88 2024-01-17
 */
final class Formatter extends XmlBase
{
    /**
     * @var string[]
     */
    private static array $calProps = [
        IcalInterface::VERSION,
        IcalInterface::PRODID,
        IcalInterface::CALSCALE,
        IcalInterface::METHOD,
    ];

    /**
     * @var string[]
     */
    private static array $calPropsrfc7986Single = [
        IcalInterface::UID,
        IcalInterface::LAST_MODIFIED,
        IcalInterface::URL,
        IcalInterface::REFRESH_INTERVAL,
        IcalInterface::SOURCE,
        IcalInterface::COLOR
    ];

    /**
     * @var string[]
     */
    private static array $calPropsrfc7986Multi = [
        IcalInterface::NAME,
        IcalInterface::DESCRIPTION,
        IcalInterface::CATEGORIES,
        IcalInterface::IMAGE
    ];

    /**
     * @var string
     */
    private static string $boolean        = 'boolean';

    /**
     * @var string
     */
    private static string $integer        = 'integer';

    /**
     * @var string
     */
    private static string $rstatus        = 'rstatus';

    /**
     * @var string
     */
    private static string $time           = 'time';

    /**
     * @var string
     */
    private static string $utc_offset     = 'utc-offset';


    /**
     * @var string
     */
    private static string $bysecond       = 'bysecond';

    /**
     * @var string
     */
    private static string $byminute       = 'byminute';

    /**
     * @var string
     */
    private static string $byhour         = 'byhour';

    /**
     * @var string
     */
    private static string $bymonthday = 'bymonthday';

    /**
     * @var string
     */
    private static string $byyearday  = 'byyearday';

    /**
     * @var string
     */
    private static string $byweekno   = 'byweekno';

    /**
     * @var string
     */
    private static string $bymonth    = 'bymonth';

    /**
     * @var string
     */
    private static string $bysetpos   = 'bysetpos';

    /**
     * @var string
     */
    private static string $byday      = 'byday';

    /**
     * @var string
     */
    public static string $XMLstart =
        '<?xml version="1.0" encoding="utf-8"?><icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0"><!-- kigkonsult.se %s, iCal2XMl (rfc6321), %s --></icalendar>';

    /**
     * Return iCal XML (rfc6321) string output, false on error
     *
     * @param Vcalendar $calendar iCalcreator Vcalendar instance
     * @return bool|string
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    public static function iCal2XML( Vcalendar $calendar ) : bool | string
    {
        static $YMDTHISZ = 'Ymd\THis\Z';
        // fix an SimpleXMLElement instance and create root element
        $xml       = new SimpleXMLElement(
            sprintf( self::$XMLstart, ICALCREATOR_VERSION, gmdate( $YMDTHISZ ))
        );
        $vCalendarXml = self::addSubChild( $xml, self::$Vcalendar );
        $langCal      = $calendar->getConfig( IcalInterface::LANGUAGE );
        // fix calendar properties
        self::calendarProps2Xml( $calendar, $vCalendarXml, $langCal );
        // prepare to fix components with properties/subComponents
        $componentsXml = self::addSubChild( $vCalendarXml, self::$components );
        // fix component properties
        foreach( $calendar->getComponents() as $component ) {
            $componentChild = self::addSubChild( $componentsXml, strtolower( $component->getCompType()));
            self::componentProps2Xml( $component, $componentChild, $langCal );
        } // end foreach
        return $xml->asXML();
    }

    /**
     * Parse Vcalendar properties into XML
     *
     * @param Vcalendar $calendar iCalcreator Vcalendar instance
     * @param SimpleXMLElement $vCalendarXml
     * @param bool|string $langCal
     * @throws Exception
     * @since 2.41.69 2022-10-07
     */
    private static function calendarProps2Xml(
        Vcalendar $calendar,
        SimpleXMLElement $vCalendarXml,
        bool | string $langCal
    ) : void
    {
        $properties = self::addSubChild( $vCalendarXml, self::$properties );
        foreach( self::$calProps as $propName ) {
            $method = StringFactory::getGetMethodName( $propName );
            if( false !== ( $content = $calendar->{$method}())) {
                self::addXMLchildText( $properties, $propName, Pc::factory( $content ));
            }
        } // end foreach
        foreach( self::$calPropsrfc7986Single as $propName ) {
            $getMethod = StringFactory::getGetMethodName( $propName );
            if( false === ( $content = $calendar->{$getMethod}( true ))) {
                continue;
            }
            match( strtoupper( $propName )) {
                IcalInterface::UID, IcalInterface::COLOR =>
                    self::addXMLchildText( $properties, $propName, $content ),
                IcalInterface::LAST_MODIFIED =>
                    self::addXMLchildDateTime( $properties, $propName, $content, true ),
                IcalInterface::SOURCE, IcalInterface::URL =>
                    self::addXMLchildUri( $properties, $propName, $content ),
                IcalInterface::REFRESH_INTERVAL =>
                    self::addXMLchildDuration( $properties, $propName, $content ),
                default => null
            }; // end match
        } // end foreach
        foreach( self::$calPropsrfc7986Multi as $propName ) {
            $method = StringFactory::getGetAllMethodName( $propName );
            switch( strtoupper( $propName )) {
                case IcalInterface::CATEGORIES :  // fall through
                case IcalInterface::DESCRIPTION : // fall through
                case IcalInterface::NAME :
                    foreach( $calendar->{$method}( true ) as $content ) {
                        self::addXMLchildText( $properties, $propName, $content, $langCal );
                    } // end while
                    break;
                case IcalInterface::IMAGE :
                    self::addXMLchildBinaryUriArr( $properties, $propName, $calendar->{$method}( true )); // array
                    break;
            } // end switch
        } // end foreach
        foreach( $calendar->getAllXprop( true ) as $contents ) {
            self::addXMLchild( $properties, $contents[0], self::$unknown, $contents[1] );
        } // end while
    }

    /**
     * Parse component into XML
     *
     * @param CalendarComponent|Vevent $component
     * @param SimpleXMLElement $parentXml
     * @param bool|string $langCal
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function componentProps2Xml(
        CalendarComponent|Vevent $component,
        SimpleXMLElement $parentXml,
        bool | string $langCal
    ) : void
    {
        $compName   = $component->getCompType();
        $properties = self::addSubChild( $parentXml, self::$properties );
        $langComp   = $component->getConfig( IcalInterface::LANGUAGE );
        $lang       = ( $langComp ?: $langCal );
        $props      = $component->getConfig( IcalInterface::SETPROPERTYNAMES );
        foreach( $props as $propName ) {
            if( StringFactory::isXprefixed( $propName )) {
                $content = $component->getXprop( $propName, null, true );
                self::addXMLchild( $properties, $content[0], self::$unknown, $content[1] );
                continue;
            }
            $method = Vcalendar::isMultiProp( $propName )
                ? StringFactory::getGetAllMethodName( $propName )
                : StringFactory::getGetMethodName( $propName );
            $data   = $component->{$method}( true );
            match( strtoupper( $propName )) {
                IcalInterface::ATTACH,           // may occur multiple times
                IcalInterface::IMAGE =>
                    self::addXMLchildBinaryUriArr( $properties, $propName, $data ), // array
                IcalInterface::ATTENDEE =>        // may occur multiple times
                    self::addXMLchildCalAddress( $properties, $propName, $data, $langComp ),
                IcalInterface::EXDATE =>
                    self::addXMLchildExdate( $properties, $propName, $data ),
                IcalInterface::FREEBUSY =>
                    self::addXMLchildFreebusy( $properties, $propName, $data ),
                IcalInterface::REQUEST_STATUS =>
                    self::addXMLchildReqStat( $properties, $propName, $data, $langComp, $langCal ),
                IcalInterface::RDATE =>
                    self::addXMLchildRdate( $properties, $propName, $data),
                IcalInterface::DESCRIPTION,   // multiple in VCALENDAR/VJOURNAL, single elsewere
                IcalInterface::LOCATION =>    // multiple in PARTICIPANT, single elsewere
                    self::addXMLchildDescrLoc( $component, $properties, $propName, $data, $lang, $compName ),
                IcalInterface::STYLED_DESCRIPTION =>
                    self::addXMLchildStlDescr( $properties, $propName, $data, $lang ),
                IcalInterface::CATEGORIES, IcalInterface::COMMENT,
                IcalInterface::CONTACT,       //   // single in VFREEBUSY, multiple elsewhere
                IcalInterface::NAME,          // dito, multi i Vcalendar, single in Vlocation/Vresource (here)
                IcalInterface::TZID_ALIAS_OF, IcalInterface::TZNAME,
                IcalInterface::RESOURCES, IcalInterface::RELATED_TO =>
                    self::addXMLchildGroup1( $component, $properties, $propName, $data, $langComp, $langCal, $compName ),
                IcalInterface::ACKNOWLEDGED, IcalInterface::CREATED, IcalInterface::COMPLETED,
                IcalInterface::DTSTAMP, IcalInterface::LAST_MODIFIED,
                IcalInterface::DTSTART, IcalInterface::DTEND, IcalInterface::DUE,
                IcalInterface::RECURRENCE_ID, IcalInterface::TZUNTIL =>
                    self::addXMLchildGroup2( $properties, $propName, $data ),
                IcalInterface::DURATION  =>
                    self::addXMLchildDuration( $properties, $propName, $data ),
                IcalInterface::EXRULE, IcalInterface::RRULE =>
                    self::addXMLchildRecur( $properties, $propName, $data ),
                IcalInterface::LOCATION_TYPE, IcalInterface::SUMMARY, IcalInterface::ACTION,
                IcalInterface::BUSYTYPE, IcalInterface::KLASS, IcalInterface::COLOR,
                IcalInterface::PROXIMITY, IcalInterface::PARTICIPANT_TYPE, IcalInterface::RESOURCE_TYPE,
                IcalInterface::STATUS, IcalInterface::TRANSP, IcalInterface::TZID, IcalInterface::UID =>
                    self::addXMLchildGroup3( $properties, $propName, $data, $langComp, $langCal ),
                IcalInterface::GEO =>
                    self::addXMLchild( $properties, $propName, strtolower( IcalInterface::GEO ), $data ),
                IcalInterface::CALENDAR_ADDRESS, IcalInterface::ORGANIZER =>
                    self::addXMLchildCalAddress( $properties, $propName, [ $data ], $lang ),
                IcalInterface::PERCENT_COMPLETE, IcalInterface::PRIORITY,
                IcalInterface::REPEAT, IcalInterface::SEQUENCE =>
                    self::addXMLchildInteger( $properties, $propName, $data ),
                IcalInterface::STRUCTURED_DATA =>
                    self::addXMLchildStrData( $properties, $propName, $data ),
                IcalInterface::CONFERENCE =>
                    self::addXMLchildConferens( $properties, $propName, $data ),
                IcalInterface::TRIGGER =>
                    self::addXMLchildTrigger( $properties, $propName, $data ),
                IcalInterface::TZOFFSETFROM, IcalInterface::TZOFFSETTO =>
                    self::addXMLchild( $properties, $propName, self::$utc_offset, $data ),
                IcalInterface::TZURL, IcalInterface::URL =>
                    self::addXMLchildUri( $properties, $propName, $data ),
                default => null
            }; // end match( $propName )
        } // end foreach( $props as $pix => $propName )
        // fix subComponent properties, if any
        foreach( $component->getComponents() as $subComp ) {
            $subCompChild =self::addSubChild(  $parentXml, strtolower( $subComp->getCompType()));
            self::componentProps2Xml( $subComp, $subCompChild, $langCal );
        } // end foreach
    }

    /**
     * Manage Exdate
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildExdate(
        SimpleXMLElement $properties,
        string $propName,
        array $data
    ) : void
    {
        foreach( $data as $content ) {
            if( self::hasValueDateSet( $content )) {
                self::addXMLchildDate( $properties, $propName, $content );
                continue;
            }
            self::addXMLchildDateTime( $properties, $propName, $content );
        } // end foreach
    }

    /**
     * Manage FREEBUSY
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildFreebusy(
        SimpleXMLElement $properties,
        string $propName,
        array $data
    ) : void
    {
        foreach( $data as $content ) {
            self::addXMLchild( $properties, $propName, self::$period, $content );
        } // end foreach
    }

    /**
     * Manage REQUEST_STATUS
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @param bool | string $langComp
     * @param bool | string $langCal
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildReqStat(
        SimpleXMLElement $properties,
        string $propName,
        array $data,
        bool | string $langComp,
        bool | string $langCal
    ) : void
    {
        foreach( $data as $content ) {
            self::addLanguage( $content, $langComp, $langCal );
            self::addXMLchild( $properties, $propName, self::$rstatus, $content );
        } // end foreach
    }

    /**
     * Manage Rdate
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildRdate(
        SimpleXMLElement $properties,
        string $propName,
        array $data
    ) : void
    {
        foreach( $data as $content ) {
            $type = self::getRdateValueType( $content );
            self::addXMLchild( $properties, $propName, $type, $content );
        } // end foreach
    }

    /**
     * Manage DESCRIPTION+LOCATION
     *
     * @param CalendarComponent|Vevent $component
     * @param SimpleXMLElement $properties
     * @param string      $propName
     * @param array       $data
     * @param bool|string $lang
     * @param string      $compName
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildDescrLoc(
        CalendarComponent|Vevent $component,
        SimpleXMLElement $properties,
        string $propName,
        array $data,
        bool|string $lang,
        string $compName
    ) : void
    {
        foreach( $data as $content ) {
            self::addXMLchildText( $properties, $propName, $content, $lang );
            if(( IcalInterface::DESCRIPTION === $propName ) &&
                $component::isDescriptionSingleProp( $compName )) {
                break;
            }
            if(( IcalInterface::LOCATION === $propName ) &&
                $component::isLocationSingleProp( $compName )) {
                break;
            }
        } // end foreach
    }

    /**
     * Manage STYLED_DESCRIPTION
     *
     * @param SimpleXMLElement $properties
     * @param string      $propName
     * @param array       $data
     * @param bool|string $lang
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildStlDescr(
        SimpleXMLElement $properties,
        string $propName,
        array $data,
        bool|string $lang
    ) : void
    {
        foreach( $data as $content ) {
            if( $content->hasParamValue( IcalInterface::URI )) {
                self::addXMLchildUri( $properties, $propName, $content );
                continue;
            }
            self::addXMLchildText( $properties, $propName, $content, $lang );
        } // end foreach
    }

    /**
     * Manage CATEGORIES+COMMENT+CONTACT+NAME+TZID_ALIAS_OF+TZNAME+RESOURCES+RELATED_TO
     *
     * @param CalendarComponent|Vevent $component
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @param bool | string $langComp
     * @param bool | string $langCal
     * @param string $compName
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildGroup1(
        CalendarComponent|Vevent $component,
        SimpleXMLElement $properties,
        string $propName,
        array $data,
        bool | string $langComp,
        bool | string $langCal,
        string $compName
    ) : void
    {
        foreach( $data as $content ) {
            if( $propName !== IcalInterface::RELATED_TO ) {
                self::addLanguage( $content, $langComp, $langCal );
            }
            self::addXMLchildText( $properties, $propName, $content );
            if( ( IcalInterface::NAME === $propName ) ||
                ( ( IcalInterface::CONTACT === $propName ) &&
                    $component::isContactSingleProp( $compName ) ) ) {
                break;
            }
        } // end foreach
    }

    /**
     * Manage ACKNOWLEDGED+CREATED+COMPLETED+DTSTAMP+LAST_MODIFIED+DTSTART+DTEND+DUE+RECURRENCE_IDTZUNTIL
     *
     * @param SimpleXMLElement $properties
     * @param string $propName
     * @param Pc     $content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildGroup2(
        SimpleXMLElement $properties,
        string $propName,
        Pc $content
    ) : void
    {
        if( self::hasValueDateSet( $content )) {
            self::addXMLchildDate( $properties, $propName, $content );
        }
        else {
            self::addXMLchildDateTime( $properties, $propName, $content );
        }
    }

    /**
     * Manage group3 of properties
     *
     * LOCATION_TYPE+SUMMARY+ACTION+BUSYTYPE+KLASS+COLOR+PROXIMITY+PARTICIPANT_TYPE+
     * RESOURCE_TYPE+STATUS+TRANSP+TZID+UID :
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param Pc               $content
     * @param bool | string    $langCal
     * @param bool | string    $langComp
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildGroup3(
        SimpleXMLElement $properties,
        string $propName,
        Pc $content,
        bool | string $langComp,
        bool | string $langCal
    ) : void
    {
        static $LOCTSUM = [ IcalInterface::LOCATION_TYPE, IcalInterface::SUMMARY ];
        if( in_array( $propName, $LOCTSUM, true )) {
            self::addLanguage( $content, $langComp, $langCal );
        }
        self::addXMLchildText( $properties, $propName, $content );
    }

    /**
     * Manage STRUCTURED_DATA
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildStrData(
        SimpleXMLElement $properties,
        string $propName,
        array $data
    ) : void
    {
        foreach( $data as $content ) {
            if( $content->hasParamValue( IcalInterface::TEXT )) {
                self::addXMLchildText( $properties, $propName, $content );
                continue;
            }
            self::addXMLchildBinaryUri( $properties, $propName, $content );
        } // end foreach
    }

    /**
     * Manage CONFERENCE
     *
     * @param SimpleXMLElement $properties
     * @param string           $propName
     * @param array            $data
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildConferens(
        SimpleXMLElement $properties,
        string $propName,
        array $data
    ) : void
    {
        foreach( $data as $content ) {
            self::addXMLchildUri( $properties, $propName, $content );
        }
    }

    /**
     * Manage TRIGGER
     *
     * @param SimpleXMLElement $properties
     * @param string $propName
     * @param Pc     $content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 2024-01-16
     */
    private static function addXMLchildTrigger(
        SimpleXMLElement $properties,
        string $propName,
        Pc $content
    ) : void
    {
        if( $content->getValue() instanceof DateInterval ) {
            self::addXMLchildDuration( $properties, $propName, $content );
        }
        else {
            self::addXMLchildDateTime( $properties, $propName, $content );
        }
    }

    /**
     * Add Pc content parameter language if not set
     *
     * @param Pc $content  property content
     * @param bool|string $langComp
     * @param bool|string $langCal
     * @return void
     * @since 2.41.88 2024-01-17
     */
    private static function addLanguage( Pc $content, bool | string $langComp, bool | string $langCal ) : void
    {
        match( true ) {
            $content->hasParamKey( IcalInterface::LANGUAGE ) => null,
            ( ! empty( $langComp )) =>
                $content->addParam( IcalInterface::LANGUAGE, $langComp ),
            ( ! empty( $langCal )) =>
                $content->addParam( IcalInterface::LANGUAGE, $langCal ),
            default => null,
        }; // end match
    }

    /**
     * Return bool true if VALUE=DATE, remove param VALUE
     *
     * @param Pc $content
     * @return bool
     */
    private static function hasValueDateSet( Pc $content ) : bool
    {
        $isDateSet = $content->hasParamValue( IcalInterface::DATE );
        $content->removeParam( IcalInterface::VALUE );
        return $isDateSet;
    }

    /**
     * @param Pc $content
     * @return string
     */
    private static function getRdateValueType( Pc $content ) : string
    {
        $type     = self::$date_time;
        if( $content->hasParamValue( IcalInterface::DATE )) {
            $type = self::$date;
        }
        elseif( $content->hasParamValue( IcalInterface::PERIOD )) {
            $type = self::$period;
        } // end if
        $content->removeParam( IcalInterface::VALUE );
        return $type;
    }

    /**
     * Add XML (rfc6321) binary/uri children to SimpleXMLelement from array
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc[]             $contents new subelements content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-05
     */
    private static function addXMLchildBinaryUriArr( SimpleXMLElement $parent, string $name, array $contents ) : void
    {
        foreach( $contents as $content ) {
            self::addXMLchildBinaryUri( $parent, $name, $content );
        }
    }

    /**
     * Add XML (rfc6321) binary/uri children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildBinaryUri( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        $type = $content->hasParamValue( IcalInterface::BINARY )
            ? self::$binary
            : self::$uri;
        $content->removeParam( IcalInterface::VALUE );
        self::addXMLchild( $parent, $name, $type, $content );
    }

    /**
     * Add XML (rfc6321) cal-address children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent a SimpleXMLelement class instance
     * @param string $name new element node name
     * @param Pc[] $contents new subelement contents
     * @param bool|string $lang
     * @return void
     * @throws Exception
     * @since 2.41.69 2022-11-02
     */
    private static function addXMLchildCalAddress(
        SimpleXMLElement $parent,
        string $name,
        array $contents,
        bool|string $lang
    ) : void
    {
        foreach( $contents as $content ) {
            if( $content->hasParamKey( IcalInterface::CN )) {
                self::addLanguage( $content, $lang, false );
            }
            self::addXMLchild( $parent, $name, self::$cal_address, $content );
        } // end foreach
    }

    /**
     * Add XML (rfc6321) date children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement contents
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildDate( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        self::addXMLchild( $parent, $name, self::$date, $content );
    }

    /**
     * Add XML (rfc6321) date-time children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildDateTime(
        SimpleXMLElement $parent,
        string $name,
        Pc $content,
        ? bool $skipValue = false
    ) : void
    {
        if( $skipValue ) {
            $content->removeParam(IcalInterface::VALUE );
        }
        self::addXMLchild( $parent, $name, self::$date_time, $content );
    }

    /**
     * Add XML (rfc6321) duration children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildDuration( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        self::addXMLchild( $parent, $name, strtolower( IcalInterface::DURATION ), $content );
    }

    /**
     * Add XML (rfc6321) integer children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildInteger( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        self::addXMLchild( $parent, $name, self::$integer, $content );
    }

    /**
     * Add XML (rfc6321) recur children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildRecur( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        self::addXMLchild( $parent, $name, self::$recur, $content );
    }

    /**
     * Add XML (rfc6321) text children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent   a SimpleXMLelement class instance
     * @param string           $name     new element node name
     * @param Pc               $content  new subelement content
     * @param null|bool|string $lang
     * @return void
     * @throws Exception
     * @since 2.41.69 2022-10-04
     */
    private static function addXMLchildText(
        SimpleXMLElement $parent,
        string $name,
        Pc $content,
        null|bool|string $lang = false
    ) : void
    {
        if( $lang ) {
            self::addLanguage( $content, false, $lang );
        }
        self::addXMLchild( $parent, $name, self::$text, $content );
    }

    /**
     * Add XML (rfc6321) uri children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param Pc               $content new subelement content
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 - 2024-01-18
     */
    private static function addXMLchildUri( SimpleXMLElement $parent, string $name, Pc $content ) : void
    {
        $content->setValue( htmlentities( $content->getValue()));
        self::addXMLchild( $parent, $name, self::$uri, $content );
    }

    /**
     * Add XML (rfc6321) children to SimpleXMLelement
     *
     * @param SimpleXMLElement $parent  a SimpleXMLelement class instance
     * @param string           $name    new element node name
     * @param string           $type    content type, subelement(-s) name
     * @param Pc               $subData new subelement value and 'attributes'
     * @return void
     * @throws Exception
     * @throws InvalidArgumentException
     * @since 2.41.88 - 2024-01-18
     */
    private static function addXMLchild(
        SimpleXMLElement $parent,
        string $name,
        string $type,
        Pc $subData
    ) : void
    {
        /** create new child node */
        $name  = strtolower( $name );
        $child = self::addSubChild( $parent, $name );
        $value = $subData->getValue();
        if( empty( $value ) && ( StringFactory::$ZERO !== (string) $value )) {
            self::addSubChild( $child, $type );
            return;
        }
        $recurDateIsSet = false;
        $isLocalTime    = $subData->hasParamIsLocalTime();
        $params         = (array) $subData->getParams();
        $isOneParm      = ( 1 === count( $params ));
        switch( true ) {
            case ( empty( $params ) || ( $isOneParm && $isLocalTime )) :
                break;
            case ( self::$recur === $type ) :
                $recurDateIsSet = $subData->hasParamValue( IcalInterface::DATE );
                if( $isOneParm && $subData->hasParamValue()) {
                    break;
                }
                $subData->removeParam( IcalInterface::VALUE );
                // fall through
            default :
                $parameters = self::addSubChild( $child, self::$PARAMETERS );
                self::addXMLchildParams( $parameters, $type, $params );
                break;
        } // end switch
        /** store content dep. on type */
        self::addXMLchildForType( $child, $type, $subData->getValue(), $isLocalTime, $recurDateIsSet );
    }

    /**
     * @param SimpleXMLElement $parameters
     * @param string           $type
     * @param array            $params
     * @return void
     * @throws Exception
     */
    private static function addXMLchildParams( SimpleXMLElement $parameters, string $type, array $params ) : void
    {
        static $altrep       = 'altrep';
        static $dir          = 'dir';
        static $delegated_from = 'delegated-from';
        static $delegated_to = 'delegated-to';
        static $member       = 'member';
        static $order        = 'order';
        static $sent_by      = 'sent-by';
        static $rsvp         = 'rsvp';
        static $derived      = 'derived';
        foreach( $params as $pKey => $parVal ) {
            if( IcalInterface::VALUE === $pKey ) {
                if( str_contains( $parVal, StringFactory::$COLON )) {
                    $p1   = self::addSubChild( $parameters, strtolower( $pKey ));
                    self::addSubChild( $p1, self::$unknown, htmlspecialchars( $parVal ));
                    $type = strtolower( strstr( $parVal, StringFactory::$COLON, true ));
                }
                elseif( 0 !== strcasecmp( $type, $parVal )) {
                    $type = strtolower( $parVal );
                }
                continue;
            } // end if VALUE
            if( IcalInterface::ISLOCALTIME === $pKey ) {
                continue;
            }
            $pKey = strtolower( $pKey );
            if( StringFactory::isXprefixed( $pKey )) {
                $p1 = self::addSubChild( $parameters, $pKey );
                self::addSubChild( $p1, self::$unknown, htmlspecialchars( $parVal ));
                continue;
            }
            $p1 = self::addSubChild( $parameters, $pKey );
            $ptype = match ( $pKey ) {
                $altrep, $dir   => self::$uri,
                $delegated_from, $delegated_to, $member, $sent_by => self::$cal_address,
                $order          => self::$integer,
                $rsvp, $derived => self::$boolean,
                default         => self::$text,
            }; // end match
            if( is_array( $parVal )) {
                foreach( $parVal as $pV ) {
                    self::addSubChild( $p1, $ptype, htmlspecialchars((string) $pV ));
                }
            }
            else {
                self::addSubChild( $p1, $ptype, htmlspecialchars((string) $parVal ));
            }
        } // end foreach $params
    }

    /**
     * @param SimpleXMLElement $child
     * @param string $type
     * @param mixed $value
     * @param bool $isLocalTime
     * @param bool $recurDateIsSet
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForType(
        SimpleXMLElement $child,
        string $type,
        mixed $value,
        bool $isLocalTime,
        bool $recurDateIsSet
    ) : void
    {
        match( $type ) {
            self::$boolean, self::$time => null,
            self::$binary, self::$uri, self::$cal_address =>
               self::addSubChild( $child, $type, $value ),
            self::$date =>
                self::addXMLchildForTypeDate( $child, $type, $value ),
            self::$date_time =>
                self::addXMLchildForTypeDateTime( $child, $type, $value, $isLocalTime ),
            strtolower( IcalInterface::DURATION ) =>
                self::addSubChild( $child, $type, DateIntervalFactory::dateInterval2String( $value, true )),
            strtolower( IcalInterface::GEO ) =>
                self::addXMLchildForTypeGeo( $child, $value ),
            self::$integer =>
                self::addSubChild( $child, $type, (string) $value ),
            self::$period =>
                self::addXMLchildForTypePeriod( $child, $type, $value, $isLocalTime ),
            self::$recur =>
                self::addXMLchildForTypeRecur( $child, $value, $recurDateIsSet ),
            self::$rstatus =>
                self::addXMLchildForTypeRstatus( $child, $value ),
            self::$text =>
                self::addXMLchildForTypeText( $child, $type, $value ),
            self::$utc_offset =>
                self::addXMLchildForTypeUtcOffset( $child, $type, $value ),
            default => // also self::$unknown
                self::addXMLchildForTypeUnknowDefault( $child, $value ),
        }; // end match
    }

    /**
     * Manage date type
     *
     * @param SimpleXMLElement $child
     * @param string           $type
     * @param mixed            $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeDate( SimpleXMLElement $child, string $type, mixed $value ) : void
    {
        if( $value instanceof DateTime ) {
            $value = [ $value ];
        }
        foreach( $value as $date ) {
            self::addSubChild( $child, $type, DateTimeFactory::dateTime2Str( $date, true ));
        }
    }

    /**
     * Manage date-time type
     *
     * @param SimpleXMLElement $child
     * @param string           $type
     * @param mixed            $value
     * @param bool $isLocalTime
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeDateTime(
        SimpleXMLElement $child,
        string $type,
        mixed $value,
        bool $isLocalTime
    ) : void
    {
        if( $value instanceof DateTime ) {
            $value = [ $value ];
        }
        foreach( $value as $dt ) {
            self::addSubChild( $child, $type, DateTimeFactory::dateTime2Str( $dt, false, $isLocalTime ));
        } // end foreach
    }

    /**
     * Manage geo type
     *
     * @param SimpleXMLElement $child
     * @param mixed            $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeGeo( SimpleXMLElement $child, mixed $value ) : void
    {
        if( ! empty( $value )) {
            self::addSubChild(
                $child,
                IcalInterface::LATITUDE,
                GeoFactory::geo2str2( $value[IcalInterface::LATITUDE], GeoFactory::$geoLatFmt )
            );
            self::addSubChild(
                $child,
                IcalInterface::LONGITUDE,
                GeoFactory::geo2str2( $value[IcalInterface::LONGITUDE], GeoFactory::$geoLatFmt )
            );
        } // end if
    }

    /**
     * Manage period type
     *
     * @param SimpleXMLElement $child
     * @param string $type
     * @param mixed  $value
     * @param bool   $isLocalTime
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypePeriod(
        SimpleXMLElement $child,
        string $type,
        mixed $value,
        bool $isLocalTime
    ) : void
    {
        static $START = 'start';
        static $END   = 'end';
        if( ! is_array( $value )) {
            return;
        }
        foreach( $value as $period ) {
            $v1  = self::addSubChild( $child, $type );
            $str = DateTimeFactory::dateTime2Str( $period[0], false, $isLocalTime );
            self::addSubChild( $v1, $START, $str );
            if( $period[1] instanceof DateInterval ) {
                self::addSubChild(
                    $v1,
                    strtolower( IcalInterface::DURATION ),
                    DateIntervalFactory::dateInterval2String( $period[1] )
                );
            }
            elseif( $period[1] instanceof DateTime ) {
                $str = DateTimeFactory::dateTime2Str( $period[1], false, $isLocalTime );
                self::addSubChild( $v1, $END, $str );
            }
        } // end foreach
    }

    /**
     * Manage recur type
     *
     * @param SimpleXMLElement $child
     * @param mixed  $value
     * @param bool   $recurDateIsSet
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeRecur(
        SimpleXMLElement $child,
        mixed $value,
        bool $recurDateIsSet
    ) : void
    {
        static $UNTIL = 'until';
        $value        = array_change_key_case( $value );
        foreach( $value as $ruleLabel => $ruleValue ) {
            match( $ruleLabel ) {
                $UNTIL =>
                    self::addSubChild( $child, $ruleLabel, DateTimeFactory::dateTime2Str( $ruleValue, $recurDateIsSet )),
                self::$bysecond, self::$byminute, self::$byhour,
                self::$bymonthday, self::$byyearday, self::$byweekno, self::$bymonth, self::$bysetpos =>
                    self::addXMLchildForTypeRecurGroup1( $child, $ruleLabel, $ruleValue ),
                self::$byday =>
                    self::addXMLchildForTypeRecurByDay( $child, $ruleLabel, $ruleValue ),
                default => // self::$freq, self::$count, self::$interval, self::$wkst
                    self::addSubChild( $child, $ruleLabel, (string) $ruleValue ),
            }; // end match( $ruleLabel )
        } // end foreach( $value as $ruleLabel => $ruleValue )
    }

    /**
     * @param SimpleXMLElement $child
     * @param string           $ruleLabel
     * @param mixed            $ruleValue
     * @return void
     * @throws Exception
     */
     private static function addXMLchildForTypeRecurGroup1(
         SimpleXMLElement $child,
         string $ruleLabel,
         mixed $ruleValue
     ) : void
     {
         if( is_array( $ruleValue )) {
             foreach( $ruleValue as $valuePart ) {
                 self::addSubChild( $child, $ruleLabel, (string) $valuePart );
             }
         }
         else {
             self::addSubChild( $child, $ruleLabel, $ruleValue );
         }
    }

    /**
     * @param SimpleXMLElement $child
     * @param string           $ruleLabel
     * @param mixed            $ruleValue
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeRecurByDay(
        SimpleXMLElement $child,
        string $ruleLabel,
        mixed $ruleValue
    ) : void
    {
        if( isset( $ruleValue[IcalInterface::DAY] )) {
            $str  = $ruleValue[0] ?? StringFactory::$SP0;
            $str .= $ruleValue[IcalInterface::DAY];
            self::addSubChild( $child, $ruleLabel, $str );
        }
        else {
            foreach( $ruleValue as $valuePart ) {
                if( isset( $valuePart[IcalInterface::DAY] )) {
                    $str  = $valuePart[0] ?? StringFactory::$SP0;
                    $str .= $valuePart[IcalInterface::DAY];
                    self::addSubChild( $child, $ruleLabel, $str );
                }
                else {
                    self::addSubChild( $child, $ruleLabel, $valuePart );
                }
            } // end foreach
        }
    }

    /**
     * Manage rstatus type
     *
     * @param SimpleXMLElement $child
     * @param mixed            $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeRstatus( SimpleXMLElement $child, mixed $value ) : void
    {
        self::addSubChild(
            $child,
            self::$code,
            StringFactory::numberFormat( $value[IcalInterface::STATCODE] )
        );
        self::addSubChild( $child, self::$description, htmlspecialchars( $value[IcalInterface::STATDESC] ));
        if( isset( $value[IcalInterface::EXTDATA] )) {
            self::addSubChild( $child, self::$data, htmlspecialchars( $value[IcalInterface::EXTDATA] ));
        }
    }

    /**
     * Manage text type
     *
     * @param SimpleXMLElement $child
     * @param string $type
     * @param mixed  $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeText( SimpleXMLElement $child, string $type, mixed $value ) : void
    {
        if( ! is_array( $value )) {
            $value = [ $value ];
        }
        foreach( $value as $part ) {
            self::addSubChild( $child, $type, htmlspecialchars( $part ) );
        }
    }

    /**
     * Manage utc_offset type
     *
     * @param SimpleXMLElement $child
     * @param string $type
     * @param mixed  $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeUtcOffset( SimpleXMLElement $child, string $type, mixed $value ) : void
    {
        if( DateIntervalFactory::hasPlusMinusPrefix( $value )) {
            $str   = $value[0];
            $value = substr( $value, 1 );
        }
        else {
            $str = StringFactory::$PLUS;
        }
        $str .= substr( $value, 0, 2 ) .
            StringFactory::$COLON . substr( $value, 2, 2 );
        if( 4 < strlen( $value )) {
            $str .= StringFactory::$COLON . substr( $value, 4 );
        }
        self::addSubChild( $child, $type, $str );
    }

    /**
     * Manage unknown and default type
     *
     * @param SimpleXMLElement $child
     * @param mixed  $value
     * @return void
     * @throws Exception
     */
    private static function addXMLchildForTypeUnknowDefault( SimpleXMLElement $child, mixed $value ) : void
    {
        if( is_array( $value )) {
            $value = implode( $value );
        }
        self::addSubChild( $child, self::$unknown, htmlspecialchars( $value ));
    }

    /**
     * @param SimpleXMLElement $child
     * @param string           $name
     * @param null|string      $value
     * @return SimpleXMLElement
     * @throws Exception
     */
    private static function addSubChild( SimpleXMLElement $child, string $name, ? string $value = null ) : SimpleXMLElement
    {
        $subChild = $child->addChild( $name, $value );
        if( null === $subChild ) {
            throw new Exception( 'Can\'t add XML child ' . $name );
        }
        return $subChild;
    }
}