<?php
/*
 +--------------------------------------------------------------------+
 | Copyright CiviCRM LLC. All rights reserved.                        |
 |                                                                    |
 | This work is published under the GNU AGPLv3 license with some      |
 | permitted exceptions and without any warranty. For full license    |
 | and copyright information, see https://civicrm.org/licensing       |
 +--------------------------------------------------------------------+

 */

/**
 * @package CRM
 * @copyright CiviCRM LLC https://civicrm.org/licensing
 */
class CRM_Event_BAO_Participant extends CRM_Event_DAO_Participant implements \Civi\Core\HookInterface {

  /**
   * Static field for all the participant information that we can potentially import.
   *
   * @var array
   */
  public static $_importableFields = NULL;

  /**
   * Static field for all the participant information that we can potentially export.
   *
   * @var array
   */
  public static $_exportableFields = NULL;

  /**
   * Static array for valid status transitions rules.
   *
   * @var array
   */
  public static $_statusTransitionsRules = [
    'Pending from pay later' => ['Registered', 'Cancelled'],
    'Pending from incomplete transaction' => ['Registered', 'Cancelled'],
    'On waitlist' => ['Cancelled', 'Pending from waitlist'],
    'Pending from waitlist' => ['Registered', 'Cancelled'],
    'Awaiting approval' => ['Cancelled', 'Pending from approval'],
    'Pending from approval' => ['Registered', 'Cancelled'],
  ];

  /**
   * @deprecated
   * @return CRM_Event_BAO_Participant
   */
  public static function add($params) {
    return self::writeRecord($params);
  }

  /**
   * Given the list of params in the params array, fetch the object
   * and store the values in the values array
   *
   * @param array $params
   *   Input parameters to find object.
   * @param array $values
   *   Output values of the object.
   *
   * @param $ids
   *
   * @return CRM_Event_BAO_Participant|null the found object or null
   */
  public static function getValues(&$params, &$values = [], &$ids = []) {
    if (empty($params)) {
      return NULL;
    }
    $participant = new CRM_Event_BAO_Participant();
    $participant->copyValues($params);
    $participant->find();
    $participants = [];
    while ($participant->fetch()) {
      $ids['participant'] = $participant->id;
      CRM_Core_DAO::storeValues($participant, $values[$participant->id]);
      $participants[$participant->id] = $participant;
    }
    return $participants;
  }

  /**
   * Takes an associative array and creates a participant object.
   *
   * @param array $params
   *   (reference ) an assoc array of name/value pairs.
   *
   * @return CRM_Event_BAO_Participant
   */
  public static function create(&$params) {

    $transaction = new CRM_Core_Transaction();
    $status = NULL;

    if (!empty($params['id'])) {
      $status = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant', $params['id'], 'status_id');
    }

    $participant = self::add($params);
    $participant->find(TRUE);

    // Log activity when creating new participant or changing status
    if (empty($params['id']) ||
      (isset($params['status_id']) && $params['status_id'] != $status)
    ) {
      // Default status if not specified
      $participant->status_id = $participant->status_id ?: self::fields()['participant_status_id']['default'];
      CRM_Activity_BAO_Activity::addActivity($participant, 'Event Registration', $participant->contact_id);
    }

    //CRM-5403
    //for update mode
    if (self::isPrimaryParticipant($participant->id) && $status) {
      self::updateParticipantStatus($participant->id, $status, $participant->status_id);
    }

    $session = CRM_Core_Session::singleton();
    $id = $session->get('userID');
    if (!$id) {
      $id = $params['contact_id'] ?? NULL;
    }

    //process note, CRM-7634
    $noteId = NULL;
    if (!empty($params['id'])) {
      $note = CRM_Core_BAO_Note::getNote($params['id'], 'civicrm_participant');
      $noteId = key($note);
    }
    $noteValue = NULL;
    $hasNoteField = FALSE;
    foreach (['note', 'participant_note'] as $noteFld) {
      if (array_key_exists($noteFld, $params)) {
        $noteValue = $params[$noteFld];
        $hasNoteField = TRUE;
        break;
      }
    }
    if ($noteId || $noteValue) {
      if ($noteValue) {
        $noteParams = [
          'entity_table' => 'civicrm_participant',
          'note' => $noteValue,
          'entity_id' => $participant->id,
          'id' => $noteId,
        ];
        CRM_Core_BAO_Note::add($noteParams);
      }
      elseif ($noteId && $hasNoteField) {
        CRM_Core_BAO_Note::deleteRecord(['id' => $noteId]);
      }
    }

    // Log the information on successful add/edit of Participant data.
    $logParams = [
      'entity_table' => 'civicrm_participant',
      'entity_id' => $participant->id,
      'data' => CRM_Event_PseudoConstant::participantStatus($participant->status_id),
      'modified_id' => $id,
      'modified_date' => date('Ymd'),
    ];

    CRM_Core_BAO_Log::add($logParams);

    $params['participant_id'] = $participant->id;

    $transaction->commit();

    // do not add to recent items for import, CRM-4399
    if (empty($params['skipRecentView'])) {

      $url = CRM_Utils_System::url('civicrm/contact/view/participant',
        "action=view&reset=1&id={$participant->id}&cid={$participant->contact_id}&context=home"
      );

      $recentOther = [];
      if (CRM_Core_Permission::check('edit event participants')) {
        $recentOther['editUrl'] = CRM_Utils_System::url('civicrm/contact/view/participant',
          "action=update&reset=1&id={$participant->id}&cid={$participant->contact_id}&context=home"
        );
      }
      if (CRM_Core_Permission::check('delete in CiviEvent')) {
        $recentOther['deleteUrl'] = CRM_Utils_System::url('civicrm/participant/delete',
          "reset=1&id={$participant->id}"
        );
      }

      $participantRoles = CRM_Event_PseudoConstant::participantRole();

      if ($participant->role_id) {
        $role = explode(CRM_Core_DAO::VALUE_SEPARATOR, $participant->role_id);

        foreach ($role as & $roleValue) {
          if (isset($roleValue)) {
            $roleValue = $participantRoles[$roleValue];
          }
        }
        $roles = implode(', ', $role);
      }

      $roleString = empty($roles) ? '' : $roles;
      $eventTitle = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $participant->event_id, 'title');
      $title = CRM_Contact_BAO_Contact::displayName($participant->contact_id) . ' (' . $roleString . ' - ' . $eventTitle . ')';

      // add the recently created Participant
      CRM_Utils_Recent::add($title,
        $url,
        $participant->id,
        'Participant',
        $participant->contact_id,
        NULL,
        $recentOther
      );
    }

    return $participant;
  }

  /**
   * @internal
   *     Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *     participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function getAvailableSpaces(int $eventId, bool $includeWaitlist = TRUE): int {
    $availableSpaces = self::eventFull(
      $eventId,
      TRUE,
      $includeWaitlist
    );
    return is_numeric($availableSpaces) ? (int) $availableSpaces : 0;
  }

  /**
   * Check whether the event is full for participation and return as.
   * per requirements.
   *
   * @param int $eventId
   *   Event id.
   * @param bool $returnEmptySeats
   *   Are we require number if empty seats.
   * @param bool $includeWaitingList
   *   Consider waiting list in event full.
   *                 calculation or not. (it is for cron job  purpose)
   *
   * @param bool $returnWaitingCount
   * @param bool $considerTestParticipant deprecated, unused
   * @param bool $onlyPositiveStatuses
   *   When FALSE, count all participant statuses where is_counted = 1.  This includes
   *   both "Positive" participants (Registered, Attended, etc.) and waitlisted
   *   (and some pending) participants.
   *   When TRUE, count only participants with statuses of "Positive".
   *
   * @return bool|int|null|string
   *   1. false                 => If event having some empty spaces.
   * @throws \CRM_Core_Exception
   */
  public static function eventFull(
    $eventId,
    $returnEmptySeats = FALSE,
    $includeWaitingList = TRUE,
    $returnWaitingCount = FALSE,
    $considerTestParticipant = FALSE,
    $onlyPositiveStatuses = FALSE
  ) {
    $result = NULL;
    if (!$eventId) {
      return $result;
    }

    // consider event is full when.
    // 1. (count(is_counted) >= event_size) or
    // 2. (count(participants-with-status-on-waitlist) > 0)
    // It might be case there are some empty spaces and still event
    // is full, as waitlist might represent group require spaces > empty.

    $countedStatuses = \CRM_Event_BAO_Participant::buildOptions('status_id', NULL, ['is_counted' => 1]);;
    $positiveStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Positive'");
    $waitingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Waiting'");
    $onWaitlistStatusId = array_search('On waitlist', $waitingStatuses);

    $where = [' event.id = %1 ', ' participant.is_test = 0 '];
    $participantRoleClause = self::getParticipantRoleClause();
    if ($participantRoleClause) {
      $where[] = " participant.role_id " . $participantRoleClause;
    }
    $eventParams = [1 => [$eventId, 'Positive']];

    //in case any waiting, straight forward event is full.
    if ($includeWaitingList && $onWaitlistStatusId) {

      //build the where clause.
      $whereClause = ' WHERE ' . implode(' AND ', $where);
      $whereClause .= " AND participant.status_id = $onWaitlistStatusId ";
      $eventSeatsWhere = implode(' AND ', $where) . " AND ( participant.status_id = $onWaitlistStatusId )";

      $query = "
    SELECT  participant.id id
      FROM  civicrm_participant participant
INNER JOIN  civicrm_event event ON ( event.id = participant.event_id )
            {$whereClause}";

      $hasWaitlistedParticipants = CRM_Core_DAO::singleValueQuery($query, $eventParams);
      if ($hasWaitlistedParticipants) {
        //oops here event is full and we don't want waiting count.
        if ($returnWaitingCount) {
          return CRM_Event_BAO_Event::eventTotalSeats($eventId, $eventSeatsWhere);
        }
        return CRM_Core_DAO::singleValueQuery('SELECT event_full_text FROM civicrm_event WHERE id = ' . (int) $eventId) ?: ts('This event is full.');
      }
    }

    //Consider only counted participants, or alternatively only registered (not on waitlist) participants.
    if ($onlyPositiveStatuses) {
      $where[] = ' participant.status_id IN ( ' . implode(', ', array_keys($positiveStatuses)) . ' ) ';
    }
    else {
      $where[] = ' participant.status_id IN ( ' . implode(', ', array_keys($countedStatuses)) . ' ) ';
    }
    $whereClause = ' WHERE ' . implode(' AND ', $where);
    $eventSeatsWhere = implode(' AND ', $where);

    $query = "
    SELECT  participant.id id,
            event.event_full_text as event_full_text,
            event.max_participants as max_participants
      FROM  civicrm_participant participant
INNER JOIN  civicrm_event event ON ( event.id = participant.event_id )
            {$whereClause}";

    $eventMaxSeats = NULL;
    $eventFullText = ts('This event is full.');
    $participants = CRM_Core_DAO::executeQuery($query, $eventParams);
    while ($participants->fetch()) {
      if ($participants->event_full_text) {
        $eventFullText = $participants->event_full_text;
      }
      $eventMaxSeats = $participants->max_participants;
      //don't have limit for event seats.
      if ($participants->max_participants == NULL) {
        return $result;
      }
    }

    //get the total event seats occupied by these participants.
    $eventRegisteredSeats = CRM_Event_BAO_Event::eventTotalSeats($eventId, $eventSeatsWhere);

    if ($eventRegisteredSeats) {
      if ($eventRegisteredSeats >= $eventMaxSeats) {
        $result = $eventFullText;
      }
      elseif ($returnEmptySeats) {
        $result = $eventMaxSeats - $eventRegisteredSeats;
      }
      return $result;
    }
    else {
      $query = '
SELECT  event.event_full_text,
        event.max_participants
  FROM  civicrm_event event
 WHERE  event.id = %1';
      $event = CRM_Core_DAO::executeQuery($query, $eventParams);
      while ($event->fetch()) {
        $eventFullText = $event->event_full_text;
        $eventMaxSeats = $event->max_participants;
      }
    }

    // no limit for registration.
    if ($eventMaxSeats == NULL) {
      return $result;
    }
    if ($eventMaxSeats) {
      return ($returnEmptySeats) ? (int) $eventMaxSeats : FALSE;
    }

    return $eventFullText;
  }

  /**
   * Return the array of all price set field options,
   * with total participant count that field going to carry.
   *
   * @param int $eventId
   *   Event id.
   * @param array $skipParticipantIds
   *   An array of participant ids those we should skip.
   * @param bool $considerCounted
   * @param bool $considerWaiting
   * @param bool $considerTestParticipants
   *
   * @return array
   *   an array of each option id and total count
   */
  public static function priceSetOptionsCount(
    $eventId,
    $skipParticipantIds = [],
    $considerCounted = TRUE,
    $considerWaiting = TRUE,
    $considerTestParticipants = FALSE
  ) {
    $optionsCount = [];
    if (!$eventId) {
      return $optionsCount;
    }

    $allStatusIds = [];
    if ($considerCounted) {
      $countedStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, 'is_counted = 1');
      $allStatusIds = array_merge($allStatusIds, array_keys($countedStatuses));
    }
    if ($considerWaiting) {
      $waitingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Waiting'");
      $allStatusIds = array_merge($allStatusIds, array_keys($waitingStatuses));
    }
    $statusIdClause = NULL;
    if (!empty($allStatusIds)) {
      $statusIdClause = ' AND participant.status_id IN ( ' . implode(', ', array_values($allStatusIds)) . ')';
    }

    $isTestClause = NULL;
    if (!$considerTestParticipants) {
      $isTestClause = ' AND participant.is_test = 0 ';
    }

    $skipParticipantClause = NULL;
    if (is_array($skipParticipantIds) && !empty($skipParticipantIds)) {
      $skipParticipantClause = ' AND participant.id NOT IN ( ' . implode(', ', $skipParticipantIds) . ')';
    }

    $sql = "
    SELECT  line.id as lineId,
            line.entity_id as entity_id,
            line.qty,
            value.id as price_field_value_id,
            value.count,
            field.html_type
      FROM  civicrm_line_item line
INNER JOIN  civicrm_participant participant ON ( line.entity_table  = 'civicrm_participant'
                                                 AND participant.id = line.entity_id )
INNER JOIN  civicrm_price_field_value value ON ( value.id = line.price_field_value_id )
INNER JOIN  civicrm_price_field field       ON ( value.price_field_id = field.id )
     WHERE  participant.event_id = %1
       AND  line.qty > 0
            {$statusIdClause}
            {$isTestClause}
            {$skipParticipantClause}";

    $lineItem = CRM_Core_DAO::executeQuery($sql, [1 => [$eventId, 'Positive']]);
    while ($lineItem->fetch()) {
      $id = (int) $lineItem->price_field_value_id;
      $optionsCount[$id] ??= 0;
      $count = $lineItem->count ?: 1;
      if ($lineItem->html_type === 'Text') {
        $count *= $lineItem->qty;
      }
      $optionsCount[$id] += $count;
    }

    return $optionsCount;
  }

  /**
   * Combine all the importable fields from the lower levels object.
   *
   * @param string $contactType
   * @param bool $status
   * @param bool $onlyParticipant
   * @param bool $checkPermission
   *   Is this a permissioned retrieval?
   *
   * @deprecated since 5.74 will be removed around 5.86
   *
   * @return array
   *   array of importable Fields
   */
  public static function &importableFields($contactType = 'Individual', $status = TRUE, $onlyParticipant = FALSE, $checkPermission = TRUE) {
    CRM_Core_Error::deprecatedFunctionWarning('use apiv4');
    if (!self::$_importableFields) {
      if (!$onlyParticipant) {
        if (!$status) {
          $fields = ['' => ['title' => ts('- do not import -')]];
        }
        else {
          $fields = ['' => ['title' => ts('- Participant Fields -')]];
        }
      }
      else {
        $fields = [];
      }

      $tmpFields = CRM_Event_DAO_Participant::import();

      $note = [
        'participant_note' => [
          'title' => ts('Participant Note'),
          'name' => 'participant_note',
          'headerPattern' => '/(participant.)?note$/i',
          'data_type' => CRM_Utils_Type::T_TEXT,
        ],
      ];

      // Split status and status id into 2 fields
      // Fixme: it would be better to leave as 1 field and intelligently handle both during import
      // note import undoes this - it is still here in case the search usage uses it.
      $participantStatus = [
        'participant_status' => [
          'title' => ts('Participant Status'),
          'name' => 'participant_status',
          'data_type' => CRM_Utils_Type::T_STRING,
        ],
      ];
      $tmpFields['participant_status_id']['title'] = ts('Participant Status Id');

      // Split role and role id into 2 fields
      // Fixme: it would be better to leave as 1 field and intelligently handle both during import
      // note import undoes this - it is still here in case the search usage uses it.
      $participantRole = [
        'participant_role' => [
          'title' => ts('Participant Role'),
          'name' => 'participant_role',
          'data_type' => CRM_Utils_Type::T_STRING,
        ],
      ];
      $tmpFields['participant_role_id']['title'] = ts('Participant Role Id');

      $eventType = [
        'event_type' => [
          'title' => ts('Event Type'),
          'name' => 'event_type',
          'data_type' => CRM_Utils_Type::T_STRING,
        ],
      ];

      $tmpContactField = $contactFields = [];
      $contactFields = [];
      if (!$onlyParticipant) {
        $contactFields = CRM_Contact_BAO_Contact::importableFields($contactType, NULL);

        // Using new Dedupe rule.
        $ruleParams = [
          'contact_type' => $contactType,
          'used' => 'Unsupervised',
        ];
        $fieldsArray = CRM_Dedupe_BAO_DedupeRule::dedupeRuleFields($ruleParams);

        if (is_array($fieldsArray)) {
          foreach ($fieldsArray as $value) {
            $customFieldId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_CustomField',
              $value,
              'id',
              'column_name'
            );
            $value = $customFieldId ? 'custom_' . $customFieldId : $value;
            $tmpContactField[trim($value)] = $contactFields[trim($value)] ?? NULL;
            if (!$status) {
              $title = $tmpContactField[trim($value)]['title'] . ' (match to contact)';
            }
            else {
              $title = $tmpContactField[trim($value)]['title'];
            }

            $tmpContactField[trim($value)]['title'] = $title;
          }
        }
      }
      $extIdentifier = $contactFields['external_identifier'] ?? NULL;
      if ($extIdentifier) {
        $tmpContactField['external_identifier'] = $extIdentifier;
        $tmpContactField['external_identifier']['title'] = ($extIdentifier['title'] ?? '') . ' (match to contact)';
      }
      $tmpFields['participant_contact_id']['title'] = $tmpFields['participant_contact_id']['title'] . ' (match to contact)';

      $fields = array_merge($fields, $tmpContactField);
      $fields = array_merge($fields, $tmpFields);
      $fields = array_merge($fields, $note, $participantStatus, $participantRole, $eventType);
      $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Participant', FALSE, FALSE, FALSE, $checkPermission));

      self::$_importableFields = $fields;
    }

    return self::$_importableFields;
  }

  /**
   * Combine all the exportable fields from the lower level objects.
   *
   * @param bool $checkPermission
   *
   * @return array
   *   array of exportable Fields
   */
  public static function &exportableFields($checkPermission = TRUE) {
    if (!self::$_exportableFields) {
      if (!self::$_exportableFields) {
        self::$_exportableFields = [];
      }

      $participantFields = CRM_Event_DAO_Participant::export();
      $eventFields = CRM_Event_DAO_Event::export();
      $noteField = [
        'participant_note' => [
          'title' => ts('Participant Note'),
          'name' => 'participant_note',
          'type' => CRM_Utils_Type::T_STRING,
        ],
      ];

      $participantStatus = [
        'participant_status' => [
          'title' => ts('Participant Status (label)'),
          'name' => 'participant_status',
          'type' => CRM_Utils_Type::T_STRING,
        ],
      ];

      $participantRole = [
        'participant_role' => [
          'title' => ts('Participant Role (label)'),
          'name' => 'participant_role',
          'type' => CRM_Utils_Type::T_STRING,
        ],
      ];

      $participantFields['participant_role_id']['title'] .= ' (ID)';

      $discountFields = CRM_Core_DAO_Discount::export();

      $fields = array_merge($participantFields, $participantStatus, $participantRole, $eventFields, $noteField, $discountFields);

      // add custom data
      $fields = array_merge($fields, CRM_Core_BAO_CustomField::getFieldsForImport('Participant', FALSE, FALSE, FALSE, $checkPermission));
      self::$_exportableFields = $fields;
    }

    return self::$_exportableFields;
  }

  /**
   * Get the event name/sort name for a particular participation / participant
   *
   * @param int $participantId
   *   Id of the participant.
   *
   * @return array
   *   associated array with sort_name and event title
   */
  public static function participantDetails($participantId) {
    $query = "
SELECT civicrm_contact.sort_name as name, civicrm_event.title as title, civicrm_contact.id as cid
FROM   civicrm_participant
   LEFT JOIN civicrm_event   ON (civicrm_participant.event_id = civicrm_event.id)
   LEFT JOIN civicrm_contact ON (civicrm_participant.contact_id = civicrm_contact.id)
WHERE  civicrm_participant.id = {$participantId}
";
    $dao = CRM_Core_DAO::executeQuery($query);

    $details = [];
    while ($dao->fetch()) {
      $details['name'] = $dao->name;
      $details['title'] = $dao->title;
      $details['cid'] = $dao->cid;
    }

    return $details;
  }

  /**
   * Get the values for pseudoconstants for name->value and reverse.
   *
   * @param array $defaults
   *   (reference) the default values, some of which need to be resolved.
   * @param bool $reverse
   *   True if we want to resolve the values in the reverse direction (value -> name).
   */
  public static function resolveDefaults(&$defaults, $reverse = FALSE) {
    self::lookupValue($defaults, 'event', CRM_Event_PseudoConstant::event(), $reverse);
    self::lookupValue($defaults, 'status', CRM_Event_PseudoConstant::participantStatus(NULL, NULL, 'label'), $reverse);
    self::lookupValue($defaults, 'role', CRM_Event_PseudoConstant::participantRole(), $reverse);
  }

  /**
   * Convert associative array names to values and vice-versa.
   *
   * This function is used by both the web form layer and the api. Note that
   * the api needs the name => value conversion, also the view layer typically
   * requires value => name conversion
   *
   * @param array $defaults
   * @param string $property
   * @param string[] $lookup
   * @param bool $reverse
   *
   * @return bool
   */
  public static function lookupValue(&$defaults, $property, $lookup, $reverse) {
    $id = $property . '_id';

    $src = $reverse ? $property : $id;
    $dst = $reverse ? $id : $property;

    if (!array_key_exists($src, $defaults)) {
      return FALSE;
    }

    $look = $reverse ? array_flip($lookup) : $lookup;

    if (is_array($look)) {
      if (!array_key_exists($defaults[$src], $look)) {
        return FALSE;
      }
    }
    $defaults[$dst] = $look[$defaults[$src]];
    return TRUE;
  }

  /**
   * Delete the records that are associated with this participation.
   *
   * @param int $id
   *   Id of the participation to delete.
   *
   * @return \CRM_Event_DAO_Participant
   */
  public static function deleteParticipant($id) {
    $participant = new CRM_Event_DAO_Participant();
    $participant->id = $id;
    if (!$participant->find()) {
      return FALSE;
    }
    CRM_Utils_Hook::pre('delete', 'Participant', $id);

    $transaction = new CRM_Core_Transaction();

    //delete activity record
    $params = [
      'source_record_id' => $id,
      // activity type id for event registration
      'activity_type_id' => 5,
    ];

    CRM_Activity_BAO_Activity::deleteActivity($params);

    // delete the participant payment record
    // we need to do this since the cascaded constraints
    // dont work with join tables
    $p = ['participant_id' => $id];
    CRM_Event_BAO_ParticipantPayment::deleteParticipantPayment($p);

    // cleanup line items.
    $participantsId = [];
    $participantsId = self::getAdditionalParticipantIds($id);
    $participantsId[] = $id;
    CRM_Price_BAO_LineItem::deleteLineItems($participantsId, 'civicrm_participant');

    //delete note when participant deleted.
    $note = CRM_Core_BAO_Note::getNote($id, 'civicrm_participant');
    $noteId = key($note);
    if ($noteId) {
      CRM_Core_BAO_Note::deleteRecord(['id' => $noteId]);
    }

    $participant->delete();

    $transaction->commit();

    CRM_Utils_Hook::post('delete', 'Participant', $participant->id, $participant);

    return $participant;
  }

  /**
   * Retrieves existing participants for a given event and contact.
   *
   * @param int $contactId
   *   The contact ID of participants to find.
   * @param int $eventId
   *   The event ID of participants to find.
   * @param bool $onlyCounted
   *   Whether to only consider registrations with a status with "is_counted".
   * @param bool $includeOnWaitlist
   *   Whether to consider registrations with status "On waitlist" when restricting to "is_counted".
   * @param array $excludeStatus
   *   A list of registration status to not consider (e.g. for ignoring cancelled registrations).
   * @param array $filterRoleIds
   *   A list of participant role IDs to filter for. Registrations with other roles will not be considered.
   * @param bool $includeTest
   *   Whether to include test participants.
   *
   * @return array<int,array{id:int,"status_id:name":string}>
   *   An array of participants (a subset of attributes) matching the given criteria, keyed by ID.
   * @throws \CRM_Core_Exception
   * @throws \Civi\API\Exception\UnauthorizedException
   *
   * @internal
   *    Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *    participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function getExistingParticipants(
    int $contactId,
    int $eventId,
    bool $onlyCounted = TRUE,
    bool $includeOnWaitlist = TRUE,
    array $excludeStatus = ['Cancelled'],
    array $filterRoleIds = [],
    bool $includeTest = FALSE
  ) {
    $query = \Civi\Api4\Participant::get(FALSE)
      ->addSelect('id', 'status_id:name')
      ->addWhere('contact_id', '=', $contactId)
      ->addWhere('event_id', '=', $eventId);

    if ($onlyCounted) {
      $query->addJoin('ParticipantStatusType AS participant_status_type', 'LEFT');
      $clauses[] = ['participant_status_type.is_counted', '=', TRUE];
      if ($includeOnWaitlist) {
        $clauses[] = ['participant_status_type.name', '=', 'On waitlist'];
      }
      $query->addClause('OR', $clauses);
    }

    if ([] !== $excludeStatus) {
      $query->addWhere('status_id:name', 'NOT IN', $excludeStatus);
    }

    if ([] !== $filterRoleIds) {
      $query->addWhere('role_id', 'CONTAINS ONE OF', $filterRoleIds);
    }

    if (!$includeTest) {
      $query->addWhere('is_test', '=', FALSE);
    }

    $result = $query->execute();
    return $result->getArrayCopy();
  }

  /**
   * @param int $contactId
   * @param int $eventId
   * @param string $context
   *   Either "admin" or "public" depending on the context of the validation/form.
   * @param bool $isAdditional
   *   Whether to validate for an additional participant.
   *
   * @return array<int|string,string>
   *   A list of validation errors, possibly keyed by attribute name the error corresponds to.
   * @throws \CRM_Core_Exception
   *
   * @internal
   *    Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *    participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function validateExistingRegistration(
    int $contactId,
    int $eventId,
    string $context = 'public',
    bool $isAdditional = FALSE
  ): array {
    $event = \Civi\Api4\Event::get(FALSE)
      ->addSelect(
        'default_role_id',
        'allow_same_participant_emails'
      )
      ->addWhere('id', '=', $eventId)
      ->execute()
      ->single();
    $excludeStatus = 'admin' === $context ? ['Cancelled'] : [];
    $participantRoleIds = 'public' === $context ? [$participantRoleId ?? $event['default_role_id']] : [];
    $existingParticipants = CRM_Event_BAO_Participant::getExistingParticipants(
      $contactId,
      $eventId,
      TRUE,
      TRUE,
      $excludeStatus,
      $participantRoleIds
    );
    $errors = [];

    if (count($existingParticipants) > 0) {
      if ($isAdditional) {
        $errors[] = ts("It looks like this participant is already registered for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
      }
      elseif (!(bool) $event['allow_same_participant_emails']) {
        if ('admin' === $context) {
          $errors['event_id'] = ts('This contact has already been assigned to this event.');
        }
        else {
          if ('On waitlist' === reset($existingParticipants)['status_id:name']) {
            $errors[] = ts("It looks like you are already waitlisted for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
          }
          else {
            $errors[] = ts("It looks like you are already registered for this event. If you want to change your registration, or you feel that you've received this message in error, please contact the site administrator.");
          }
        }
      }
    }

    return $errors;
  }

  /**
   * @param array $values
   *   A list of values to validate, keyed by participant properties/field names.
   * @param string $context
   *   The context for which to create error messages. Either "public" or "admin".
   *
   * @return array<int|string,string>
   *   A list of validation error messages, possibly keyed by affected participant properties/field names.
   *
   * @internal
   *    Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *    participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function validateAvailableSpaces(
    array $values,
    string $context = 'public'
  ): array {
    $errors = [];
    $eventId = $values['event_id'];
    $event = \Civi\Api4\Event::get(FALSE)
      ->addSelect('has_waitlist', 'max_participants')
      ->addWhere('id', '=', $eventId)
      ->execute()
      ->single();

    $availableSpaces = CRM_Event_BAO_Participant::eventFull(
      $eventId,
      TRUE,
      $event['has_waitlist']
    );
    $spacesAvailable = is_numeric($availableSpaces) ? (int) $availableSpaces : 0;

    //check for availability of registrations.
    if ($event['max_participants'] !== NULL
      && !empty($values['additional_participants'])
      && empty($values['bypass_payment']) &&
      ((int) $values['additional_participants']) >= $spacesAvailable
    ) {
      $errors['additional_participants'] = ts(
        'There is only enough space left on this event for %1 participant(s).',
        [1 => $spacesAvailable]
      );
    }

    return $errors;
  }

  /**
   * @param int $eventId
   *
   * @return array<int|string,string>
   *   A list of validation error messages, possibly keyed by affected participant properties/field names.
   * @throws \CRM_Core_Exception
   *
   * @internal
   *   Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *   participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function validateEvent(int $eventId): array {
    $event = \Civi\Api4\Event::get(FALSE)
      ->addWhere('id', '=', $eventId)
      ->execute()
      ->single();
    $errors = [];

    // is the event active (enabled)?
    if (!$event['is_active']) {
      // form is inactive, die a fatal death
      $errors[] = ts('The event you requested is currently unavailable (contact the site administrator for assistance).');
    }

    // is online registration is enabled?
    if (!$event['is_online_registration']) {
      $errors[] = ts('Online registration is not currently available for this event (contact the site administrator for assistance).');
    }

    // is this an event template ?
    if (!empty($event['is_template'])) {
      $errors[] = ts('Event templates are not meant to be registered.');
    }

    $now = date('YmdHis');
    $startDate = CRM_Utils_Date::processDate($event['registration_start_date'] ?? NULL);

    if ($startDate && ($startDate >= $now)) {
      $errors[] = ts(
        'Registration for this event begins on %1',
        [1 => CRM_Utils_Date::customFormat($event['registration_start_date'] ?? NULL)]
      );
    }

    $regEndDate = CRM_Utils_Date::processDate($event['registration_end_date'] ?? NULL);
    $eventEndDate = CRM_Utils_Date::processDate($event['end_date'] ?? NULL);
    if (($regEndDate && ($regEndDate < $now)) || (empty($regEndDate) && !empty($eventEndDate) && ($eventEndDate < $now))) {
      $endDate = CRM_Utils_Date::customFormat($event['registration_end_date'] ?? NULL);
      if (empty($regEndDate)) {
        $endDate = CRM_Utils_Date::customFormat($event['end_date'] ?? NULL);
      }
      $errors[] = ts('Registration for this event ended on %1', [1 => $endDate]);
    }

    return $errors;
  }

  /**
   * @param array $values
   *   A list of values to validate, keyed by participant properties/field names.
   * @param string $context
   *   The context for which to create error messages. Either "public" or "admin".
   * @param bool $isAdditional
   *   Whether to validate an additional participant.
   *
   * @return array<int|string,string>
   *   A list of validation error messages, possibly keyed by affected participant properties/field names.
   *
   * @internal
   *     Do not call this from outside core code. It is expected to change multiple times in the course of refactoring
   *     participant validation out of the QuickForm layer. Eventually, there will be an API action for validation.
   */
  public static function validateEventRegistration(
    array $values,
    string $context = 'public',
    bool $isAdditional = FALSE
  ): array {
    if (!isset($values['event_id'])) {
      throw new CRM_Core_Exception('Event ID is required for validating event registrations.');
    }
    if (!isset($values['contact_id'])) {
      // TODO: Support validating without a contact?
      throw new CRM_Core_Exception('Contact ID is required for validating event registrations.');
    }

    $errors = [];

    // Validate event (status, registration period, etc.).
    $errors += self::validateEvent($values['event_id']);

    // Validate existing participants for the registering contact.
    $errors += self::validateExistingRegistration($values['contact_id'], $values['event_id'], $context, $isAdditional);

    // Validate available spaces.
    $errors += self::validateAvailableSpaces($values, $context);

    // TODO: Validate against available price options.

    return $errors;
  }

  /**
   * Checks duplicate participants.
   *
   * @param array $input
   *   An assosiative array of name /value pairs.
   *   from other function
   * @param array $duplicates
   *   (reference ) an assoc array of name/value pairs.
   *
   * @return CRM_Contribute_BAO_Contribution
   */
  public static function checkDuplicate($input, &$duplicates) {
    $eventId = $input['event_id'] ?? NULL;
    $contactId = $input['contact_id'] ?? NULL;

    $clause = [];
    $input = [];

    if ($eventId) {
      $clause[] = "event_id = %1";
      $input[1] = [$eventId, 'Integer'];
    }

    if ($contactId) {
      $clause[] = "contact_id = %2";
      $input[2] = [$contactId, 'Integer'];
    }

    if (empty($clause)) {
      return FALSE;
    }

    $clause = implode(' AND ', $clause);

    $query = "SELECT id FROM civicrm_participant WHERE $clause";
    $dao = CRM_Core_DAO::executeQuery($query, $input);
    $result = FALSE;
    while ($dao->fetch()) {
      $duplicates[] = $dao->id;
      $result = TRUE;
    }
    return $result;
  }

  /**
   * Fix the event level.
   *
   * When price sets are used as event fee, fee_level is set as ^A
   * separated string. We need to change that string to comma
   * separated string before using fee_level in view mode.
   *
   * @param string $eventLevel
   *   Event_level string from db.
   */
  public static function fixEventLevel(&$eventLevel) {
    if ((substr($eventLevel, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR) &&
      (substr($eventLevel, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR)
    ) {
      $eventLevel = implode(', ', explode(CRM_Core_DAO::VALUE_SEPARATOR, substr($eventLevel, 1, -1)));
      $pos = strrpos($eventLevel, '(multiple participants)', 0);
      if ($pos) {
        $eventLevel = substr_replace($eventLevel, "", $pos - 3, 1);
      }
    }
    elseif ((substr($eventLevel, 0, 1) == CRM_Core_DAO::VALUE_SEPARATOR)) {
      $eventLevel = implode(', ', explode(CRM_Core_DAO::VALUE_SEPARATOR,
        substr($eventLevel, 0, 1)
      ));
    }
    elseif ((substr($eventLevel, -1, 1) == CRM_Core_DAO::VALUE_SEPARATOR)) {
      $eventLevel = implode(', ', explode(CRM_Core_DAO::VALUE_SEPARATOR,
        substr($eventLevel, 0, -1)
      ));
    }
  }

  /**
   * Get the ID of the default (first) participant role
   *
   * @return int
   * @throws \CRM_Core_Exception
   */
  public static function getDefaultRoleID() {
    return (int) civicrm_api3('OptionValue', 'getvalue', [
      'return' => 'value',
      'option_group_id' => 'participant_role',
      'is_active' => 1,
      'options' => ['limit' => 1, 'sort' => 'is_default DESC'],
    ]);
  }

  /**
   * Get the additional participant ids.
   *
   * @param int|null $primaryParticipantID
   *   Primary participant ID. Null should not be passed in & handling for it
   *   will be removed.
   * @param bool $excludeCancel
   *   Do not include cancelled participants.
   * @param int|null $statusID
   *   Restrict to the specified status ID.
   *
   * @return array
   *
   * @throws \Civi\Core\Exception\DBQueryException
   * @internal not supported to be called from outside of core.
   */
  public static function getAdditionalParticipantIds(?int $primaryParticipantID, bool $excludeCancel = TRUE, ?int $statusID = NULL): array {
    if (!$primaryParticipantID) {
      CRM_Core_Error::deprecatedWarning('should not be called with no IDs');
      return [];
    }
    $where = "participant.registered_by_id={$primaryParticipantID}";
    if ($excludeCancel) {
      $negativeStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Negative'");
      $where .= ' AND participant.status_id != ' . (int) array_search('Cancelled', $negativeStatuses, TRUE);
    }

    if ($statusID) {
      $where .= " AND participant.status_id = {$statusID}";
    }

    $query = "
  SELECT  participant.id
    FROM  civicrm_participant participant
   WHERE  {$where}";

    $additionalParticipantIDs = [];
    $dao = CRM_Core_DAO::executeQuery($query);
    while ($dao->fetch()) {
      $additionalParticipantIDs[$dao->id] = $dao->id;
    }
    return $additionalParticipantIDs;
  }

  /**
   * Get the amount for the undiscounted version of the field.
   *
   * Note this function is part of the refactoring process rather than the best approach.
   *
   * @param int $eventID
   * @param int $discountedPriceFieldOptionID
   * @param string $feeLevel (deprecated)
   *
   * @return null|string
   */
  public static function getUnDiscountedAmountForEventPriceSetFieldValue($eventID, $discountedPriceFieldOptionID, $feeLevel) {
    $priceSetId = CRM_Price_BAO_PriceSet::getFor('civicrm_event', $eventID, NULL);
    $params = [
      1 => [$priceSetId, 'Integer'],
    ];
    if ($discountedPriceFieldOptionID) {
      $query = "SELECT cpfv.amount FROM `civicrm_price_field_value` cpfv
LEFT JOIN civicrm_price_field cpf ON cpfv.price_field_id = cpf.id
WHERE cpf.price_set_id = %1 AND cpfv.label = (SELECT label from civicrm_price_field_value WHERE id = %2)";
      $params[2] = [$discountedPriceFieldOptionID, 'Integer'];
    }
    else {
      $query = "SELECT cpfv.amount FROM `civicrm_price_field_value` cpfv
LEFT JOIN civicrm_price_field cpf ON cpfv.price_field_id = cpf.id
WHERE cpf.price_set_id = %1 AND cpfv.label LIKE %2";
      $params[2] = [$feeLevel, 'String'];
    }
    return CRM_Core_DAO::singleValueQuery($query, $params);
  }

  /**
   * Retrieve additional participants display-names and URL to view their participant records.
   * (excludes cancelled participants automatically)
   *
   * @param int $primaryParticipantID
   *   Id of primary participant record.
   *
   * @return array
   *   $displayName => ['url' => $viewUrl, 'status' => $status]
   *   status is NULL if positive, status label otherwise.
   */
  public static function getAdditionalParticipants($primaryParticipantID) {
    $additionalParticipants = [];
    $additionalParticipantIDs = self::getAdditionalParticipantIds($primaryParticipantID, FALSE);
    if (!empty($additionalParticipantIDs)) {
      $positiveStatuses = CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Positive'");
      foreach ($additionalParticipantIDs as $additionalParticipantID) {
        $additionalContactID = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant',
          $additionalParticipantID,
          'contact_id', 'id'
        );
        $additionalContactName = CRM_Contact_BAO_Contact::displayName($additionalContactID);
        $participantCancelled = \Civi\Api4\Participant::get(TRUE)
          ->addSelect('status_id:label')
          ->addWhere('id', '=', $additionalParticipantID)
          ->addWhere('status_id:name', 'NOT IN', $positiveStatuses)
          ->execute()->first();
        $pViewURL = CRM_Utils_System::url('civicrm/contact/view/participant',
          "action=view&reset=1&id={$additionalParticipantID}&cid={$additionalContactID}"
        );

        $additionalParticipants[$additionalContactName] = [
          'url' => $pViewURL,
          'status' => $participantCancelled['status_id:label'] ?? NULL,
        ];
      }
    }
    return $additionalParticipants;
  }

  /**
   * Function for update primary and additional participant status.
   *
   * @param int $participantID
   *   Primary participant's id.
   * @param int $oldStatusID
   * @param int $newStatusID
   * @param bool $updatePrimaryStatus
   *
   * @return bool|NULL
   */
  public static function updateParticipantStatus($participantID, $oldStatusID, $newStatusID = NULL, $updatePrimaryStatus = FALSE) {
    if (!$participantID || !$oldStatusID) {
      return NULL;
    }

    if (!$newStatusID) {
      $newStatusID = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant', $participantID, 'status_id');
    }
    elseif ($updatePrimaryStatus) {
      CRM_Core_DAO::setFieldValue('CRM_Event_DAO_Participant', $participantID, 'status_id', $newStatusID);
    }

    $cascadeAdditionalIds = self::getValidAdditionalIds($participantID, $oldStatusID, $newStatusID);

    if (!empty($cascadeAdditionalIds)) {
      try {
        foreach ($cascadeAdditionalIds as $id) {
          $participantParams = [
            'id' => $id,
            'status_id' => $newStatusID,
          ];
          civicrm_api3('Participant', 'create', $participantParams);
        }
        return TRUE;
      }
      catch (CRM_Core_Exception $e) {
        throw new CRM_Core_Exception('Failed to update additional participant status in database');
      }
    }
    return FALSE;
  }

  /**
   * Function for update status for given participant ids.
   *
   * @param int $participantIds
   *   Array of participant ids.
   * @param int $statusId
   *   Status id for participant.
   * @param bool $updateRegisterDate
   */
  public static function updateStatus($participantIds, $statusId, $updateRegisterDate = FALSE) {
    if (!is_array($participantIds) || empty($participantIds) || !$statusId) {
      return;
    }

    //lets update register date as we update status to keep track
    //when we did update status, useful for moving participant
    //from pending to expired.
    $setClause = "status_id = {$statusId}";
    if ($updateRegisterDate) {
      $setClause .= ", register_date = NOW()";
    }

    $participantIdClause = '( ' . implode(',', $participantIds) . ' )';

    $query = "
UPDATE  civicrm_participant
   SET  {$setClause}
 WHERE  id IN {$participantIdClause}";

    $dao = CRM_Core_DAO::executeQuery($query);
  }

  /**
   * Function takes participant ids and statuses
   * update status from $fromStatusId to $toStatusId
   * and send mail + create activities.
   *
   * @param array $participantIds
   *   Participant ids.
   * @param int $toStatusId
   *   Update status id.
   * @param int $fromStatusId
   *   From status id.
   * @param bool $returnResult
   * @param bool $skipCascadeRule
   *
   * @return array|NULL
   */
  public static function transitionParticipants(
    $participantIds, $toStatusId,
    $fromStatusId = NULL, $returnResult = FALSE, $skipCascadeRule = FALSE
  ) {
    if (!is_array($participantIds) || empty($participantIds) || !$toStatusId) {
      return NULL;
    }

    //thumb rule is if we triggering  primary participant need to triggered additional
    $allParticipantIds = $primaryANDAdditionalIds = [];
    foreach ($participantIds as $id) {
      $allParticipantIds[] = $id;
      if (self::isPrimaryParticipant($id)) {
        //filter additional as per status transition rules, CRM-5403
        if ($skipCascadeRule) {
          $additionalIds = self::getAdditionalParticipantIds($id);
        }
        else {
          $additionalIds = self::getValidAdditionalIds($id, $fromStatusId, $toStatusId);
        }
        if (!empty($additionalIds)) {
          $allParticipantIds = array_merge($allParticipantIds, $additionalIds);
          $primaryANDAdditionalIds[$id] = $additionalIds;
        }
      }
    }

    //get the unique participant ids,
    $allParticipantIds = array_unique($allParticipantIds);

    //pull required participants, contacts, events  data, if not in hand
    static $eventDetails = [];
    static $contactDetails = [];

    $contactIds = $eventIds = $participantDetails = [];

    $statusTypes = CRM_Event_PseudoConstant::participantStatus();
    $participantRoles = CRM_Event_PseudoConstant::participantRole();
    $pendingStatuses = CRM_Event_PseudoConstant::participantStatus(NULL,
      "class = 'Pending'"
    );

    //first thing is pull all necessory data from db.
    $participantIdClause = '(' . implode(',', $allParticipantIds) . ')';

    //get all participants data.
    $query = "SELECT * FROM civicrm_participant WHERE id IN {$participantIdClause}";
    $dao = CRM_Core_DAO::executeQuery($query);
    while ($dao->fetch()) {
      $participantDetails[$dao->id] = [
        'id' => $dao->id,
        'role' => $participantRoles[$dao->role_id],
        'is_test' => $dao->is_test,
        'event_id' => $dao->event_id,
        'status_id' => $dao->status_id,
        'fee_amount' => $dao->fee_amount,
        'contact_id' => $dao->contact_id,
        'register_date' => $dao->register_date,
        'registered_by_id' => $dao->registered_by_id,
      ];
      if (!array_key_exists($dao->contact_id, $contactDetails)) {
        $contactIds[$dao->contact_id] = $dao->contact_id;
      }

      if (!array_key_exists($dao->event_id, $eventDetails)) {
        $eventIds[$dao->event_id] = $dao->event_id;
      }
    }

    //get all required contacts detail.
    if (!empty($contactIds)) {
      $contactDetails += civicrm_api3('Contact', 'get', ['id' => ['IN' => $contactIds, 'return' => 'display_name']])['values'];
    }

    //get all required events detail.
    if (!empty($eventIds)) {
      foreach ($eventIds as $eventId) {
        //retrieve event information
        $eventParams = ['id' => $eventId];
        CRM_Event_BAO_Event::retrieve($eventParams, $eventDetails[$eventId]);

        //get default participant role.
        $eventDetails[$eventId]['participant_role'] = $participantRoles[$eventDetails[$eventId]['default_role_id']] ?? NULL;
      }
    }

    //now we are ready w/ all required data.
    //take a decision as per statuses.

    $emailType = NULL;
    $toStatus = $statusTypes[$toStatusId];
    $fromStatus = $statusTypes[$fromStatusId] ?? NULL;

    switch ($toStatus) {
      case 'Pending from waitlist':
      case 'Pending from approval':
        switch ($fromStatus) {
          case 'On waitlist':
          case 'Awaiting approval':
            $emailType = 'Confirm';
            break;
        }
        break;

      case 'Expired':
        //no matter from where u come send expired mail.
        $emailType = $toStatus;
        break;

      case 'Cancelled':
        //no matter from where u come send cancel mail.
        $emailType = $toStatus;
        break;
    }

    //as we process additional w/ primary, there might be case if user
    //select primary as well as additionals, so avoid double processing.
    $processedParticipantIds = [];
    $mailedParticipants = [];

    //send mails and update status.
    foreach ($participantDetails as $participantId => $participantValues) {
      $updateParticipantIds = [];
      if (in_array($participantId, $processedParticipantIds)) {
        continue;
      }

      //check is it primary and has additional.
      if (array_key_exists($participantId, $primaryANDAdditionalIds)) {
        foreach ($primaryANDAdditionalIds[$participantId] as $additionalId) {

          if ($emailType) {
            $mail = self::sendTransitionParticipantMail($additionalId,
              $participantDetails[$additionalId],
              $eventDetails[$participantDetails[$additionalId]['event_id']],
              NULL,
              $emailType
            );

            //get the mail participant ids
            if ($mail) {
              $mailedParticipants[$additionalId] = $contactDetails[$participantDetails[$additionalId]['contact_id']]['display_name'];
            }
          }
          $updateParticipantIds[] = $additionalId;
          $processedParticipantIds[] = $additionalId;
        }
      }

      //now send email appropriate mail to primary.
      if ($emailType) {
        $mail = self::sendTransitionParticipantMail($participantId,
          $participantValues,
          $eventDetails[$participantValues['event_id']],
          NULL,
          $emailType
        );

        //get the mail participant ids
        if ($mail) {
          $mailedParticipants[$participantId] = $contactDetails[$participantValues['contact_id']]['display_name'];
        }
      }

      //now update status of group/one at once.
      $updateParticipantIds[] = $participantId;

      //update the register date only when we,
      //move participant to pending class, CRM-6496
      $updateRegisterDate = FALSE;
      if (array_key_exists($toStatusId, $pendingStatuses)) {
        $updateRegisterDate = TRUE;
      }
      self::updateStatus($updateParticipantIds, $toStatusId, $updateRegisterDate);
      $processedParticipantIds[] = $participantId;
    }

    //return result for cron.
    if ($returnResult) {
      $results = [
        'mailedParticipants' => $mailedParticipants,
        'updatedParticipantIds' => $processedParticipantIds,
      ];

      return $results;
    }
  }

  /**
   * Send mail and create activity
   * when participant status changed.
   *
   * @param int $participantId
   *   Participant id.
   * @param array $participantValues
   *   Participant detail values. status id for participants.
   * @param array $eventDetails
   *   Required event details.
   * @param array $contactDetails
   *   Required contact details.
   * @param string $mailType
   *   (eg 'approval', 'confirm', 'expired' ).
   *
   * @return bool
   */
  public static function sendTransitionParticipantMail(
    $participantId,
    $participantValues,
    $eventDetails,
    $contactDetails,
    $mailType
  ) {
    //send emails.
    $mailSent = FALSE;

    if (!$contactDetails) {
      $contactDetails = civicrm_api3('Contact', 'getsingle', [
        'id' => $participantValues['contact_id'],
        'return' => ['email', 'display_name'],
      ]);
    }
    //don't send confirmation mail to additional
    //since only primary able to confirm registration.
    if (!empty($participantValues['registered_by_id']) &&
      $mailType == 'Confirm'
    ) {
      return $mailSent;
    }

    $toEmail = $contactDetails['email'] ?? NULL;
    if ($toEmail) {

      $contactId = $participantValues['contact_id'];
      $participantName = $contactDetails['display_name'];

      //calculate the checksum value.
      $checksumValue = NULL;
      if ($mailType == 'Confirm' && !$participantValues['registered_by_id']) {
        $checksumLife = 'inf';
        $endDate = $eventDetails['end_date'] ?? NULL;
        if ($endDate) {
          $checksumLife = (CRM_Utils_Date::unixTime($endDate) - time()) / (60 * 60);
        }
        $checksumValue = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactId, NULL, $checksumLife);
      }

      //take a receipt from as event else domain.
      $receiptFrom = CRM_Core_BAO_Domain::getFromEmail();

      if (!empty($eventDetails['confirm_from_name']) && !empty($eventDetails['confirm_from_email'])) {
        $receiptFrom = $eventDetails['confirm_from_name'] . ' <' . $eventDetails['confirm_from_email'] . '>';
      }

      [$mailSent, $subject] = CRM_Core_BAO_MessageTemplate::sendTemplate(
        [
          'workflow' => 'participant_' . strtolower($mailType),
          'contactId' => $contactId,
          'tplParams' => [
            'participant' => $participantValues,
            'event' => $eventDetails,
            'paidEvent' => $eventDetails['is_monetary'] ?? NULL,
            'isShowLocation' => $eventDetails['is_show_location'] ?? NULL,
            'isAdditional' => $participantValues['registered_by_id'],
            'isExpired' => $mailType === 'Expired',
            'isConfirm' => $mailType === 'Confirm',
            'checksumValue' => $checksumValue,
          ],
          'modelProps' => [
            'participantID' => (int) $participantId,
            'eventID' => (int) $eventDetails['id'],
            'contactID' => (int) $contactId,
          ],
          'from' => $receiptFrom,
          'toName' => $participantName,
          'toEmail' => $toEmail,
          'cc' => $eventDetails['cc_confirm'] ?? NULL,
          'bcc' => $eventDetails['bcc_confirm'] ?? NULL,
        ]
      );

      // 3. create activity record.
      if ($mailSent) {
        $now = date('YmdHis');
        $activityType = 'Event Registration';
        $activityParams = [
          'subject' => $subject,
          'source_contact_id' => $contactId,
          'source_record_id' => $participantId,
          'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', $activityType),
          'activity_date_time' => CRM_Utils_Date::isoToMysql($now),
          'due_date_time' => CRM_Utils_Date::isoToMysql($participantValues['register_date']),
          'is_test' => $participantValues['is_test'],
          'status_id' => 2,
        ];
        CRM_Activity_BAO_Activity::create($activityParams);
      }
    }

    return $mailSent;
  }

  /**
   * Get participant status change message.
   *
   * @param int $participantId
   * @param $statusChangeTo
   * @param int $fromStatusId
   *
   * @return string
   */
  public static function updateStatusMessage($participantId, $statusChangeTo, $fromStatusId) {
    $statusMsg = NULL;
    $results = self::transitionParticipants([$participantId],
      $statusChangeTo, $fromStatusId, TRUE
    );

    $allStatuses = CRM_Event_PseudoConstant::participantStatus();
    //give user message only when mail has sent.
    if (is_array($results) && !empty($results)) {
      if (is_array($results['updatedParticipantIds']) && !empty($results['updatedParticipantIds'])) {
        foreach ($results['updatedParticipantIds'] as $processedId) {
          if (is_array($results['mailedParticipants']) &&
            array_key_exists($processedId, $results['mailedParticipants'])
          ) {
            $statusMsg .= '<br /> ' . ts("Participant status has been updated to '%1'. An email has been sent to %2.",
                [
                  1 => $allStatuses[$statusChangeTo],
                  2 => $results['mailedParticipants'][$processedId],
                ]
              );
          }
        }
      }
    }

    return $statusMsg;
  }

  /**
   * Get event full and waiting list message.
   *
   * @param int $eventId
   * @param int $participantId
   *
   * @return string
   */
  public static function eventFullMessage($eventId, $participantId = NULL) {
    $eventfullMsg = $dbStatusId = NULL;
    $checkEventFull = TRUE;
    if ($participantId) {
      $dbStatusId = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Participant', $participantId, 'status_id');
      if (array_key_exists($dbStatusId, CRM_Event_PseudoConstant::participantStatus(NULL, 'is_counted = 1'))) {
        //participant already in counted status no need to check for event full messages.
        $checkEventFull = FALSE;
      }
    }

    //early return.
    if (!$eventId || !$checkEventFull) {
      return $eventfullMsg;
    }

    //event is truly full.
    $emptySeats = self::eventFull($eventId, FALSE, FALSE);
    if (is_string($emptySeats) && $emptySeats !== NULL) {
      $maxParticipants = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $eventId, 'max_participants');
      $eventfullMsg = ts("This event currently has the maximum number of participants registered (%1). However, you can still override this limit and register additional participants using this form.", [
        1 => $maxParticipants,
      ]) . '<br />';
    }

    $hasWaiting = FALSE;
    $waitListedCount = self::eventFull($eventId, FALSE, TRUE, TRUE);
    if (is_numeric($waitListedCount)) {
      $hasWaiting = TRUE;
      //only current processing participant is on waitlist.
      if ($waitListedCount == 1 && CRM_Event_PseudoConstant::participantStatus($dbStatusId) == 'On waitlist') {
        $hasWaiting = FALSE;
      }
    }

    if ($hasWaiting) {
      $waitingStatusId = array_search('On waitlist',
        CRM_Event_PseudoConstant::participantStatus(NULL, "class = 'Waiting'")
      );
      $viewWaitListUrl = CRM_Utils_System::url('civicrm/event/search',
        "reset=1&force=1&event={$eventId}&status={$waitingStatusId}"
      );

      $eventfullMsg .= ts("There are %2 people currently on the waiting list for this event. You can <a href='%1'>view waitlisted registrations here</a>, or you can continue and register additional participants using this form.",
        [
          1 => $viewWaitListUrl,
          2 => $waitListedCount,
        ]
      );
    }

    return $eventfullMsg;
  }

  /**
   * Check for whether participant is primary or not.
   *
   * @param int $participantId
   *
   * @return bool
   *   true if participant is primary
   */
  public static function isPrimaryParticipant($participantId) {

    $participant = new CRM_Event_DAO_Participant();
    $participant->registered_by_id = $participantId;

    if ($participant->find(TRUE)) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Get additional participant Ids for cascading with primary participant status.
   *
   * @param int $participantId
   *   Participant id.
   * @param int $oldStatusId
   *   Previous status.
   * @param int $newStatusId
   *   New status.
   *
   * @return array
   */
  public static function getValidAdditionalIds($participantId, $oldStatusId, $newStatusId) {

    $additionalParticipantIds = [];

    static $participantStatuses = [];

    if (empty($participantStatuses)) {
      $participantStatuses = CRM_Event_PseudoConstant::participantStatus();
    }

    if (!empty(self::$_statusTransitionsRules[$participantStatuses[$oldStatusId]]) &&
      in_array($participantStatuses[$newStatusId], self::$_statusTransitionsRules[$participantStatuses[$oldStatusId]])
    ) {
      $additionalParticipantIds = self::getAdditionalParticipantIds($participantId, TRUE, $oldStatusId);
    }

    return $additionalParticipantIds;
  }

  /**
   * Get participant record count for a Contact.
   *
   * @param int $contactID
   *
   * @return int
   *   count of participant records
   */
  public static function getContactParticipantCount($contactID) {
    $query = "SELECT count(*)
FROM     civicrm_participant
WHERE    civicrm_participant.contact_id = {$contactID} AND
         civicrm_participant.is_test = 0";
    return CRM_Core_DAO::singleValueQuery($query);
  }

  /**
   * Get participant ids by contribution id.
   *
   * @param int $contributionId
   *   Contribution Id.
   * @param bool $excludeCancelled
   *   Exclude cancelled additional participant.
   *
   * @return array
   */
  public static function getParticipantIds($contributionId, $excludeCancelled = FALSE) {

    $ids = [];
    if (!$contributionId) {
      return $ids;
    }

    // get primary participant id
    $query = "SELECT participant_id
      FROM civicrm_participant cp
      LEFT JOIN civicrm_participant_payment cpp ON cp.id = cpp.participant_id
      WHERE cpp.contribution_id = {$contributionId}
      AND cp.registered_by_id IS NULL";
    $participantPayment = CRM_Core_DAO::executeQuery($query);

    // get additional participant ids (including cancelled)
    while ($participantPayment->fetch()) {
      $ids = array_merge($ids, array_merge([
        $participantPayment->participant_id,
      ], self::getAdditionalParticipantIds($participantPayment->participant_id,
        $excludeCancelled
      )));
    }

    return $ids;
  }

  /**
   * Get additional Participant edit & view url .
   *
   * @param array $participantIds
   *   An array of additional participant ids.
   *
   * @return array
   *   Array of Urls.
   */
  public static function getAdditionalParticipantUrl($participantIds) {
    foreach ($participantIds as $value) {
      $links = [];
      $details = self::participantDetails($value);
      $viewUrl = CRM_Utils_System::url('civicrm/contact/view/participant',
        "action=view&reset=1&id={$value}&cid={$details['cid']}"
      );
      $editUrl = CRM_Utils_System::url('civicrm/contact/view/participant',
        "action=update&reset=1&id={$value}&cid={$details['cid']}"
      );
      $links[] = "<td><a href='{$viewUrl}'>" . $details['name'] . "</a></td><td></td><td><a href='{$editUrl}'>" . ts('Edit') . "</a></td>";
      $links = "<table><tr>" . implode("</tr><tr>", $links) . "</tr></table>";
      return $links;
    }
  }

  /**
   * create trxn entry if an event has discount.
   *
   * @param int $eventID
   *   Event id.
   * @param array $contributionParams
   *   Contribution params.
   *
   * @param string $feeLevel (deprecated)
   * @param int $discountedPriceFieldOptionID
   *   ID of the civicrm_price_field_value field for the discount id.
   */
  public static function createDiscountTrxn($eventID, $contributionParams, $feeLevel, $discountedPriceFieldOptionID = NULL) {
    $financialTypeID = $contributionParams['contribution']->financial_type_id;
    $total_amount = $contributionParams['total_amount'];
    if (empty($discountedPriceFieldOptionID)) {
      CRM_Core_Error::deprecatedWarning('not passing the option ID is deprecated');
    }

    $checkDiscount = CRM_Core_BAO_Discount::findSet($eventID, 'civicrm_event');
    if (!empty($checkDiscount)) {
      $mainAmount = self::getUnDiscountedAmountForEventPriceSetFieldValue($eventID, $discountedPriceFieldOptionID, $feeLevel);
      $transactionParams['from_financial_account_id'] = CRM_Contribute_PseudoConstant::getRelationalFinancialAccount(
        $financialTypeID, 'Discounts Account is');
      if (!empty($transactionParams['trxnParams']['from_financial_account_id'])) {
        $transactionParams['trxnParams']['total_amount'] = $mainAmount - $total_amount;
        $transactionParams['trxnParams']['payment_processor_id'] = NULL;
        $transactionParams['trxnParams']['payment_instrument_id'] = NULL;
        $transactionParams['trxnParams']['check_number'] = NULL;
        $transactionParams['trxnParams']['trxn_id'] = NULL;
        $transactionParams['trxnParams']['net_amount'] = NULL;
        $transactionParams['trxnParams']['fee_amount'] = NULL;
        CRM_Core_BAO_FinancialTrxn::create($transactionParams);
      }
    }
  }

  /**
   * Delete participants of contact.
   *
   * @see https://issues.civicrm.org/jira/browse/CRM-12155
   *
   * @param int $contactId
   *   Contact id.
   *
   */
  public static function deleteContactParticipant($contactId) {
    $participant = new CRM_Event_DAO_Participant();
    $participant->contact_id = $contactId;
    $participant->find();
    while ($participant->fetch()) {
      self::deleteParticipant($participant->id);
    }
  }

  /**
   * @param int $participantId
   * @param $activityType
   *
   * @throws CRM_Core_Exception
   */
  public static function addActivityForSelection($participantId, $activityType) {
    $eventId = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_Participant', $participantId, 'event_id');
    $contactId = CRM_Core_DAO::getFieldValue('CRM_Event_BAO_Participant', $participantId, 'contact_id');

    $date = CRM_Utils_Date::currentDBDate();
    $title = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $eventId, 'title');
    $subject = ts('Registration selections changed for %1', [1 => $title]);

    // activity params
    $activityParams = [
      'source_contact_id' => $contactId,
      'source_record_id' => $participantId,
      'activity_type_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', $activityType),
      'subject' => $subject,
      'activity_date_time' => $date,
      'status_id' => CRM_Core_PseudoConstant::getKey('CRM_Activity_BAO_Activity', 'activity_status_id', 'Completed'),
      'skipRecentView' => TRUE,
    ];

    // create activity with target contacts
    $id = CRM_Core_Session::getLoggedInContactID();
    if ($id) {
      $activityParams['source_contact_id'] = $id;
      $activityParams['target_contact_id'][] = $contactId;
    }
    // @todo use api & also look at duplication of similar methods.
    CRM_Activity_BAO_Activity::create($activityParams);
  }

  /**
   * Pseudoconstant condition_provider for role_id field.
   * @see \Civi\Schema\EntityMetadataBase::getConditionFromProvider
   */
  public static function alterRole(string $fieldName, CRM_Utils_SQL_Select $conditions, $params) {
    if (isset($params['values']['filter'])) {
      $conditions->where('filter = #filter', ['filter' => (int) $params['values']['filter']]);
    }
  }

  /**
   * Pseudoconstant condition_provider for status_id field.
   * @see \Civi\Schema\EntityMetadataBase::getConditionFromProvider
   */
  public static function alterStatus(string $fieldName, CRM_Utils_SQL_Select $conditions, $params) {
    if (isset($params['values']['is_counted'])) {
      $conditions->where('is_counted = #counted', ['counted' => (int) $params['values']['is_counted']]);
    }
  }

  /**
   * CRM-17797 -- Format fields and setDefaults for primary and additional participants profile
   * @param int $contactId
   * @param CRM_Core_Form $form
   */
  public static function formatFieldsAndSetProfileDefaults($contactId, &$form) {
    if (!$contactId) {
      return;
    }
    $fields = [];
    if (!empty($form->_fields)) {

      foreach ($form->_fields as $name => $fieldInfo) {
        if ((substr($name, 0, 7) == 'custom_' && !$form->_allowConfirmation
          && !CRM_Core_BAO_CustomGroup::checkCustomField(substr($name, 7), ['Participant']))
          || substr($name, 0, 12) == 'participant_') {
          continue;
        }
        $fields[$name] = $fieldInfo;
      }

      if (!empty($fields)) {
        CRM_Core_BAO_UFGroup::setProfileDefaults($contactId, $fields, $form->_defaults);
      }
    }
  }

  /**
   * Evaluate whether a participant record is eligible for self-service transfer/cancellation.  If so,
   * return additional participant/event details.
   *
   * @param int $participantId
   * @param string $url
   * @param bool $isBackOffice
   */
  public static function getSelfServiceEligibility(int $participantId, string $url, bool $isBackOffice) : array {
    $optionGroupId = CRM_Core_DAO::getFieldValue('CRM_Core_DAO_OptionGroup', 'participant_role', 'id', 'name');
    $query = "
      SELECT cpst.name as status, cpst.label as statuslabel, cov.name as role, cov.label as rolelabel, cp.fee_level, cp.fee_amount, cp.register_date, cp.status_id, ce.start_date, ce.title, cp.event_id, ce.allow_selfcancelxfer
      FROM civicrm_participant cp
      LEFT JOIN civicrm_participant_status_type cpst ON cpst.id = cp.status_id
      LEFT JOIN civicrm_option_value cov ON cov.value = cp.role_id and cov.option_group_id = {$optionGroupId}
      LEFT JOIN civicrm_event ce ON ce.id = cp.event_id
      WHERE cp.id = {$participantId}";
    $dao = CRM_Core_DAO::executeQuery($query);
    while ($dao->fetch()) {
      $details['eligible'] = TRUE;
      $details['status']  = $dao->status;
      $details['role'] = $dao->role;
      $details['fee_level'] = $dao->fee_level ? implode('<br>', CRM_Core_DAO::unSerializeField($dao->fee_level, CRM_Core_DAO::SERIALIZE_SEPARATOR_BOOKEND)) : NULL;
      $details['fee_amount'] = $dao->fee_amount;
      $details['rolelabel'] = $dao->rolelabel;
      $details['statuslabel'] = $dao->statuslabel;
      $details['register_date'] = $dao->register_date;
      $details['event_start_date'] = $dao->start_date;
      $details['allow_selfcancelxfer'] = $dao->allow_selfcancelxfer;
      $eventTitle = $dao->title;
      $eventId = $dao->event_id;
    }
    if (!$isBackOffice) {
      if (!$details['allow_selfcancelxfer']) {
        $details['eligible'] = FALSE;
        $details['ineligible_message'] = ts('This event registration can not be transferred or cancelled. Contact the event organizer if you have questions.');
        return $details;
      }
      // Verify participant status is one that can be self-cancelled
      if (!in_array($details['status'], ['Registered', 'Pending from pay later', 'On waitlist', 'Pending from incomplete transaction'])) {
        $details['eligible'] = FALSE;
        $details['ineligible_message'] = ts('You cannot transfer or cancel your registration for %1 as you are not currently registered for this event.', [1 => $eventTitle]);
        return $details;
      }
      // Determine if it's too late to self-service cancel/transfer.
      $query = "select start_date as start, selfcancelxfer_time as time from civicrm_event where id = " . $eventId;
      $dao = CRM_Core_DAO::executeQuery($query);
      while ($dao->fetch()) {
        $time_limit  = $dao->time;
        $start_date = $dao->start;
      }
      $timenow = new Datetime();
      if (isset($time_limit)) {
        $cancelHours = abs($time_limit);
        $cancelInterval = new DateInterval("PT{$cancelHours}H");
        $cancelInterval->invert = $time_limit < 0 ? 1 : 0;
        $cancelDeadline = (new Datetime($start_date))->sub($cancelInterval);
        if ($timenow > $cancelDeadline) {
          $details['eligible'] = FALSE;
          // Change the language of the status message based on whether the waitlist time limit is positive or negative.
          $afterOrPrior = $time_limit <= 0 ? ts('after') : ts('prior to');
          $moreOrLess = $time_limit <= 0 ? ts('more') : ts('fewer');
          $details['ineligible_message'] = ts("Registration for this event cannot be cancelled or transferred %1 than %2 hours %3 the event's start time. Contact the event organizer if you have questions.",
          [1 => $moreOrLess, 2 => $cancelHours, 3 => $afterOrPrior]);

        }
      }
    }
    return $details;
  }

  /**
   * Callback for hook_civicrm_pre().
   * @param \Civi\Core\Event\PreEvent $event
   * @throws CRM_Core_Exception
   */
  public static function self_hook_civicrm_pre(\Civi\Core\Event\PreEvent $event) {
    $params = &$event->params;

    // Is this still necessary?
    if (!empty($params['participant_fee_amount'])) {
      $params['participant_fee_amount'] = CRM_Utils_Rule::cleanMoney($params['participant_fee_amount']);
    }

    if ($event->action === 'create') {
      //CRM-6910
      //1. If currency present, it should be valid one.
      //2. We should have currency when amount is not null.
      $currency = $params['fee_currency'] ?? NULL;
      if ($currency || !empty($params['fee_amount'])) {
        if (!CRM_Utils_Rule::currencyCode($currency)) {
          $config = CRM_Core_Config::singleton();
          $currency = $config->defaultCurrency;
        }
        $params['fee_currency'] = $currency;
      }

      // Set the default role ID on create.
      if (empty($params['role_id'])) {
        if (!empty($params['event_id'])) {
          $params['role_id'] = CRM_Core_DAO::getFieldValue('CRM_Event_DAO_Event', $params['event_id'], 'default_role_id');
        }
        else {
          $params['role_id'] = CRM_Core_DAO::singleValueQuery('SELECT default_role_id FROM civicrm_event WHERE id = %1', [
            1 => [$params['event_id'], 'Integer'],
          ]);
        }
      }
      if (empty($params['created_id'])) {
        // Set the "created_id" field if not already set.
        // The created_id should always be the person that actually did the registration.
        // That might be the first participant, but it might be someone registering someone without registering themselves.
        // 1. Prefer logged in contact id
        // 2. Fall back to 'registered_by_id' param.
        // 3. Fall back to participant contact_id (for anonymous person registering themselves)
        $params['created_id'] = CRM_Core_Session::getLoggedInContactID();
        if (empty($params['created_id'])) {
          if (!empty($params['registered_by_id'])) {
            // No logged in contact but participant was registered by someone else.
            // Look up the contact ID of that participant and record
            $participant = \Civi\Api4\Participant::get(FALSE)
              ->addSelect('contact_id')
              ->addWhere('id', '=', $params['registered_by_id'])
              ->execute()
              ->first();
            $params['created_id'] = $participant['contact_id'];
          }
          else {
            $params['created_id'] = $params['contact_id'];
          }
        }
      }
    }
  }

  /**
   * Callback for hook_civicrm_post().
   * @param \Civi\Core\Event\PostEvent $event
   * @throws CRM_Core_Exception
   */
  public static function self_hook_civicrm_post(\Civi\Core\Event\PostEvent $event) {
    CRM_Contact_BAO_GroupContactCache::opportunisticCacheFlush();
  }

  /**
   * Get the clause to exclude uncounted participant roles.
   *
   * @internal do not call from outside core code.
   *
   * @return string
   * @throws \CRM_Core_Exception
   */
  public static function getParticipantRoleClause(): string {
    // Only count Participant Roles with the "Counted?" flag.
    $participantRoles = self::buildOptions('role_id', NULL, ['filter' => TRUE]);
    $allRoles = self::buildOptions('role_id');
    if ($participantRoles === $allRoles) {
      // Don't complicate the query if no roles are excluded.
      return '';
    }
    if (!empty($participantRoles)) {
      $escapedRoles = [];
      foreach (array_keys($participantRoles) as $participantRole) {
        $escapedRoles[] = CRM_Utils_Type::escape($participantRole, 'String');
      }

      $regexp = "([[:cntrl:]]|^)" . implode('([[:cntrl:]]|$)|([[:cntrl:]]|^)', $escapedRoles) . "([[:cntrl:]]|$)";
      $participantRoleClause = "REGEXP '{$regexp}'";
    }
    return $participantRoleClause ?? '';
  }

}
