php_Template.php

<?php

namespace FindStr;

use Handlebars\Handlebars;
use phpDocumentor\Reflection\File;

class Template {

  private static array $instance = array();

  public string $group = 'default';

  public Handlebars $handlebars;

  public string $template_dir = FINDSTR_PLUGIN_PATH . 'templates/';

  public string $template_theme_dir = 'findstr/';

  public bool $enable_search_overlay = false;

  public array $results = array();

  public array $translations = array();

  public array $handlebars_helpers = array();

    /**
     * @param string $group
     *
     * @return Template[]
     */
  public static function get_instance( string $group = 'default' ) {
    if ( empty( self::$instance[ $group ] ) ) {
      self::$instance[ $group ] = new Template( $group );
    }

    return self::$instance[ $group ];
  }

  private function __construct( $group ) {

    $this->group = $group;

    //By default, we set the post types translations
    add_action( 'wp', array( $this, 'set_post_types_translations' ) );

    /**
     * Filter the template theme directory path
     *
     * @hook findstr_template_theme_dir
     *
     * @param {string} $template_theme_dir
     *
     * @return {string} $template_theme_dir The template theme directory path
     */
    $this->template_theme_dir = apply_filters( 'findstr_template_theme_dir', $this->template_theme_dir );

    $partials_loader = new HandlebarsTemplateLoader(
      array(
        trailingslashit( get_stylesheet_directory() ) . $this->template_theme_dir,
        trailingslashit( get_template_directory() ) . $this->template_theme_dir,
        $this->template_dir,
      )
    );

    $this->handlebars = new Handlebars(
      array(
        'loader'              => $partials_loader,
        'partials_loader'     => $partials_loader,
        'enableDataVariables' => true,
      )
    );

    $this->handlebars_register_helpers();

    if ( ! empty( $group ) ) {
      add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
      add_action( 'wp_footer', array( $this, 'display_search_overlay' ) );
    }

    add_filter( 'findstr_search_results', array( $this, 'search_results_permalinks' ) );

  }


  public function enqueue_scripts() {
    $settings = ( new \FindStr\ServerSettings() )->get();

    if ( empty( $settings->serverUrl ) || empty( $settings->indexUID ) || empty( $settings->publicKey ) ) {
      return;
    }

    $assets = new \FindStr\RegisterAssets( 'front/findstr-search', 'front/findstr-search', array( 'moment' ) );
    $assets->add_data(
      'findstr',
      /**
       * Filter the front script data.
       * This filter is used to pass data to the front script, such as the configuration, translations, etc.
       * This data will be available in the front script in the global variable `findstr`
       *
       * @hook findstr_front_script_data
       *
       * @param {array} $data
       *
       * @return {array} $data
       */
      apply_filters(
        'findstr_front_script_data',
        array(
          'config'          => array(
            'host'         => $settings->serverUrl,
            'index'        => $settings->indexUID,
            'publicKey'    => $settings->publicKey,
            'defaultQuery' => wp_json_encode( Search::get_instance( 'default' )->get_default_query_args() ),
            'dateFormats'  => $this->get_date_formats(),
          ),
          'translations'    => $this->get_translations(),
          'currentLanguage' => Helpers::get_language_code(),
        )
      )
    );

    $assets->set_translations( 'findstr', FINDSTR_PLUGIN_PATH . '/languages' );
    $assets->enqueue();

    $analytics = new RegisterAssets( 'front/analytics', 'front/analytics', array( 'front/findstr-search' ) );
    $analytics->enqueue();
  }

  public static function get_date_formats( $format = null ) {
    $date_formats = array(

      /**
       * Filter the date formats.
       *
       * This filter is used to modify the date formats used in the front script, it's useful to adapt the date format to the website's language.
       *
       * "format" can be 'short', 'medium', 'long', 'full'
       *
       *
       * @hook findstr_date_format_{format}
       *
       * @param {array} $date_formats
       *
       * @return {array} $date_formats
       */
      'short'  => apply_filters( 'findstr_date_format_short', _x( 'Y-m-d', 'Frontend date format', 'findstr' ) ),
      'medium' => apply_filters( 'findstr_date_format_medium', _x( 'Y/m/d H:i:s', 'Frontend date format', 'findstr' ) ),
      'long'   => apply_filters( 'findstr_date_format_long', _x( 'F j, Y', 'Frontend date format', 'findstr' ) ),
      'full'   => apply_filters( 'findstr_date_format_full', _x( 'D j F Y, H:i:s', 'Frontend date format', 'findstr' ) ),
    );

    if ( ! empty( $format ) ) {
      return( ! empty( $date_formats[ $format ] ) ? $date_formats[ $format ] : current( $date_formats ) );
    }

    return $date_formats;
  }


  /**
   * Set a translation for a key
   *
   * @param $key
   * @param $value
   * @param $context
   *
   * @return void
   */
  public function set_translation( $key, $value, $context = 'findstr' ) {
    $this->translations[ $context ][ $key ] = $value;
  }

  public function get_translations(): array {

    /**
     * Filter the translations.
     * This filter is used to modify the translations used in the front script.
     *
     * @hook findstr_translations
     *
     * @param {array} $translations
     *
     * @return {array} $translations
     */
    return (array) apply_filters( 'findstr_translations', $this->translations );
  }


  public function set_post_types_translations() {
    $post_types = get_post_types( array( 'public' => true ), 'objects' );

    foreach ( $post_types as $post_type ) {
      $this->set_translation( $post_type->name, $post_type->label, 'post_type' );
    }
  }

  /**
   * This function is used to parse the arguments passed to the handlebars helper.
   *
   * @param $args
   *
   * @return array
   */
  private function handlebars_parse_args( $args ) {
    return str_getcsv( $args, ' ', '\'' );
  }

  /**
   * Register handlebars helpers.
   * @return void
   */
  private function handlebars_register_helpers() {

    $this->handlebars_add_helper(
      'dateI18n',
      function( $template, $context, $args, $source ) {

        //enqueue the wp date script for the dateI18n helper in handlebars JS
        wp_enqueue_script( 'wp-date' );

        $format = $this->handlebars_parse_args( $args );
        $tpl    = $template->getEngine()->loadString( $source );
        $date   = $tpl->render( $context );

        $date_format = self::get_date_formats( current( $format ) );

        return date_i18n( $date_format, strtotime( $date ) );
      }
    );

    $this->handlebars_add_helper(
      'timeSince',
      function ( $template, $context, $args, $source ) {

        $tpl  = $template->getEngine()->loadString( $source );
        $date = $tpl->render( $context );

        return human_time_diff( strtotime( $date ) );
      }
    );

    $this->handlebars_add_helper(
      'log',
      function ( $template, $context, $args, $source ) {
        if ( true !== WP_DEBUG ) {
          return '';
        }

        $args = explode( ' ', $args );
        $log  = '';
        foreach ( $args as $arg ) {
          if ( ! empty( $context->get( $arg ) ) ) {
            $log .= wp_json_encode( $context->get( $arg ) );
          } else {
            $log .= $arg;
          }
          $log .= ', ';//separate logs by comma for console.log
        }
        return '<script type="application/javascript"> console.log(' . $log . ');</script>'; //phpcs:ignore: WordPress.PHP.DevelopmentFunctions.error_log_var_export
      }
    );

    $this->handlebars_add_helper(
      'compare',
      function( $template, $context, $args, $source ) {

        $args = $this->handlebars_parse_args( $args );

        if ( count( $args ) !== 3 ) {
          return '';
        }

        $lvalue   = $context->get( $args[0] );
        $operator = str_replace( '"', '', $args[1] );
        $rvalue   = '' === $context->get( $args[2] ) ? $context->get( $args[2] ) : $args[2];

        switch ( $operator ) {
          case '==':
            // phpcs:ignore
            $result = $lvalue == $rvalue;
              break;
          case '===':
            $result = $lvalue === $rvalue;
              break;
          case '!=':
            // phpcs:ignore
            $result = $lvalue != $rvalue;
              break;
          case '!==':
            $result = $lvalue !== $rvalue;
              break;
          case '<':
            $result = $lvalue < $rvalue;
              break;
          case '>':
            $result = $lvalue > $rvalue;
              break;
          case '<=':
            $result = $lvalue <= $rvalue;
              break;
          case '>=':
            $result = $lvalue >= $rvalue;
              break;
          case '&&':
            $result = $lvalue && $rvalue;
              break;
          case '||':
            $result = $lvalue || $rvalue;
              break;
          case 'typeof':
            $result = gettype( $lvalue ) === $rvalue;
              break;
          default:
              return ''; //unsupported operator
        }

        if ( $result ) {
          $tpl = $template->getEngine()->loadString( $source );
          return $tpl->render( $context );
        }

        return '';
      }
    );

  }

  public function handlebars_add_helper( $name, $callback ) {
    $this->handlebars_helpers[] = $name;
    $this->handlebars->addHelper( $name, $callback );
  }


  public function locate_template( $template_name ) {
    $template = locate_template(
      $this->template_theme_dir . $template_name . '.php'
    );

    if ( ! empty( $template ) ) {
      $template_name = $template_name . '.php';
    } else {
      $template = locate_template(
        $this->template_theme_dir . $template_name
      );
    }

    // Get default template, .php first
    if ( ! $template ) {
      if ( is_file( FINDSTR_PLUGIN_PATH . 'templates/' . $template_name . '.php' ) ) {
        $template      = FINDSTR_PLUGIN_PATH . 'templates/' . $template_name . '.php';
        $template_name = $template . '.php';
      } else {
        $template = FINDSTR_PLUGIN_PATH . 'templates/' . $template_name;
      }
    }

    /**
     * Filter the located template.
     * Use this filter to modify the template path.
     *
     * @hook findstr_locate_template
     *
     * @param {string} $template
     * @param {string} $template_name
     *
     * @return {string} $template
     */

    return apply_filters( 'findstr_locate_template', $template, $template_name );
  }

  public function get_handlebars_template( $template_name ) {
    return $this->handlebars->getPartialsLoader()->load( $template_name );
  }

  public function get_source( $template_file ) {
    ob_start();
    include $this->locate_template( $template_file );
    return ob_get_clean();
  }

  public function the_template_source( $template_name ) {
    echo $this->get_source( $template_name ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  }

  public function render_template( $string, $data ) {
    $template = $this->handlebars->loadString( $string );
    return $template->render( $data );
  }

  public function the_render( $template_name, $data ) {
    $source = $this->get_source( $template_name );
    echo $this->render_template( $source, $data ); //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
  }


  public function display_results( $default_results = array() ) {

    if ( false === $default_results ) {
      $default_results = array();
    }

    if ( true === $default_results ) {
      $default_results = $this->get_results();
    }

    $default_results['group'] = $this->group;
    $default_results['query'] = Search::get_instance( $this->group )->get_query();

    $this->get_template( 'results-container', $default_results );
  }

  public function display_search_overlay() {

    global $overlay_already_loaded;

    /**
     * Filter to enable or disable the search overlay.
     * This filter is used to enable or disable the search overlay.
     *
     * In the front-end, the search overlay is enabled only if used in a block or in menu item.
     * If you want to enable the search overlay in the front-end with custom code, you must use this filter to enable it.
     *
     * Minimal markup example:
     * ```php
     * <a href="#" class="findstrOpenModal">Open Modal</a>
     * ```
     * OR
     * ```php
     * <button class="findstrOpenModal">Open Modal</button>
     * ```
     *
     *
     *
     * @hook findstr_enable_filter_overlay
     *
     * @param {bool} $enable_search_overlay
     *
     * @return {bool} $enable_search_overlay
     *
     */
    $this->enable_search_overlay = apply_filters( 'findstr_enable_filter_overlay', $this->enable_search_overlay );
    if ( $this->enable_search_overlay || defined( 'FINDSTR_SEARCH_MENU_ITEM' ) ) {
      if ( ! $overlay_already_loaded ) {
        $this->get_template( 'main-search-overlay' );

        ( new RegisterAssets( 'front/search-overlay', 'front/search-overlay' ) )->enqueue();

        $overlay_already_loaded = true;
      }
    }
  }


  /**
   * Get Template
   *
   * @param $template_name
   * @param $args
   * @param $template_path
   * @param $default_path
   *
   * @return void
   */
  public function get_template( $template_name, $args = array(), $template_path = '', $default_path = '' ) {

    $template_file = $this->locate_template( $template_path . $template_name . '.php' );

    if ( ! file_exists( $template_file ) ) {
      $template_name = Helpers::camel2dash( $template_name );
      $template_file = $this->locate_template( $default_path . $template_name . '.php' );
    }

    if ( file_exists( $template_file ) ) {

      /**
       * Filter the template arguments.
       * Before the template is loaded arguments can be modified.
       *
       * @hook findstr_template_args
       *
       * @param {array} $args
       * @param {string} $template_name
       *
       * @return {array} $args
       */
      $args = apply_filters( 'findstr_template_args', $args, $template_name );

      /**
       * Fires before the template part is loaded.
       *
       * @hook findstr_before_template_part
       *
       * @param {string} $template_name Name of the template part.
       * @param {string} $template_path Path to the template part.
       * @param {string} $template_file Path to the template file.
       * @param {array} $args Arguments passed to the template.
       *
       */
      do_action( 'findstr_before_template_part', $template_name, $template_path, $template_file, $args );

      include $template_file;

      /**
       * Fires after the template part is loaded.
       *
       * @hook findstr_after_template_part
       *
       * @param {string} $template_name Name of the template part.
       * @param {string} $template_path Path to the template part.
       * @param {string} $template_file Path to the template file.
       * @param {array} $args Arguments passed to the template.
       *
       */
      do_action( 'findstr_after_template_part', $template_name, $template_path, $template_file, $args );
    }

  }

  /**
   * Display a field.
   *
   * @param string $slug The field slug.
   * @param array $args The field arguments, data will be available in the template.
   * @return false|void
   */
  public function display_field( string $slug, array $args = array() ) {

    $field = ( new \FindStr\Fields() )->get_field( $slug );

    if ( empty( $field ) ) {
      return false;
    }

    $args['fieldId']    = $field->id;
    $args['fieldGroup'] = $this->group;

    $args['field'] = $field;

    if ( ! empty( $this->get_field_queried_search( $field ) ) ) {
      $args['value'] = $this->get_field_queried_search( $field );
    }

    /**
     * Filter the field arguments.
     * Before the field is displayed arguments can be modified.
     *
     * @hook findstr_field_args
     *
     * @param {array} $args
     * @param {string} $slug
     * @param {string} $group
     *
     * @return {array} $args
     */
    $args = apply_filters( 'findstr_field_args', $args, $slug, $this->group );

    if ( ! empty( $args['field'] ) ) {
      $field = $args['field'];
      if ( ! empty( $field->type ) ) {
        ( new RegisterAssets( 'front/fields-' . $field->type ) )->enqueue();
      }
    }

    $this->get_template( 'fields/base-field', $args );

  }

  /**
   * @Deprecated use display_field instead
   *
   * @param string $slug
   * @param array $args
   * @return void
   */
  public function get_field( string $slug, array $args = array() ) {
    $this->display_field( $slug, $args );
  }

  public function get_search_button( $args ): string {

    $args = wp_parse_args(
      $args,
      array(
        'classes' => array( '' ),
      )
    );

    /**
     * Filter the search button arguments
     *
     * @hook findstr_search_button_args
     *
     * @param {array} $args
     *
     * @return {array} $args
     */
    $args = apply_filters( 'findstr_search_button_args', $args );

    $classes = implode( ' ', $args['classes'] );

    $search_button_html = '<button id="findstr-search-trigger" class="' . $classes . '">
    <i class="icon icon-recherche" aria-hidden="true"></i>
    <span class="sr-only">' . esc_html_x( 'Open the search window.', 'button aria label', 'findstr' ) . '</span>
  </button>';

    /**
     * Filter the search button html
     *
     * @hook findstr_search_button_html
     *
     * @param {string} $search_button_html
     * @param {array} $args
     *
     * @return {string} $search_button_html
     */
    return apply_filters( 'findstr_search_button_html', $search_button_html, $args );
  }


  /**
   * Use this function to set the default query for a group
   *
   * @param $query
   *
   * @return array
   */
  public function update_query( $query ): array {
    return Search::get_instance( $this->group )->update_query( $query );
  }

  public function set_query_arg( $key, $value ) {
      Search::get_instance( $this->group )->set_query_arg( $key, $value );
  }

  public function add_filter( $key, $value, string $compare = '=' ) {
      Search::get_instance( $this->group )->add_filter( $key, $value, $compare );
  }

  /**
   * Use this function to get the results for a query.
   * Mainly used for php rendering.
   *
   *
   * @param bool $force_query
   *
   * @return array
   */
  public function get_results( bool $force_query = false ): array {

    if ( defined( 'DOING_FINDSTR_INDEX' ) && true === DOING_FINDSTR_INDEX ) {
      return array();
    }

    if ( empty( $this->results ) || $force_query ) {
      try {

        $search         = Search::get_instance( $this->group );
        $search_results = $search->fetch_results()->toArray();

        $search_results['translations'] = $this->get_translations();

        $search_results['hits'] = array_map(
          function ( $hit ) {
            $hit['post_type_label'] = ! empty( get_post_type_object( $hit['post_type'] ) ) ? get_post_type_object( $hit['post_type'] )->label : $hit['post_type'];
            return $hit;
          },
          $search_results['hits']
        );

        $language_code = Helpers::get_language_code();
        $results_count = $search_results['facetDistribution']['language'][ $language_code ] ?? $search_results['totalHits'];

        $search_results['totalHits'] = $results_count;

        $this->results = $search_results;

      } catch ( \Exception $e ) {
        new Log( 'Get results error', 'error', array( 'message' => $e->getMessage() ) );
        return array();
      }
    }

    /**
     * Filter the results for a group
     *
     * @hook findstr_search_results
     *
     * @param {array} $results
     * @param {string} $group
     *
     * @return {array} $results
     */
    return apply_filters( 'findstr_search_results', $this->results, $this->group );
  }


  /**
   * Get queried search for group
   *
   * @param string $type
   *
   * @return array|string
   */
  public function get_queried_search( string $type = 'filter' ) {
    $queried_search = (array) get_query_var( 'findstr' );

    //unsplash all values in the array (with key "value")
    $queried_search = wp_unslash( $queried_search );

    // If we have a search query for this group and type.
    if ( ! empty( $queried_search ) &&
         ! empty( $queried_search['group'] ) &&
         $queried_search['group'] === $this->group &&
         ! empty( $queried_search[ $type ] ) ) {

      return $queried_search[ $type ];
    }

    return array();
  }

  public function get_query() {
    return Search::get_instance( $this->group )->get_query();
  }

  public function get_field_queried_search( $field ) {

    //map type to query var.
    switch ( $field->type ) {
      case 'search':
        $type = 'q';
          break;
      case 'sort':
        $type = 'sort';
          break;
      case 'pagination':
      case 'loadMore':
        $type = 'page';
          break;
      default:
        $type = 'filter';
          break;
    }

    $queried = $this->get_queried_search( $type );

    if ( empty( $queried ) ) {
      return array();
    }

    if ( ! empty( $queried[ $field->id ] ) ) {
      return $queried[ $field->id ];
    }

    if ( ! empty( $queried[ $field->source_name ] ) ) {
      return $queried[ $field->source_name ];
    }

    return $queried;
  }


  public function build_url( $args = array() ) {

    $url = '?' . http_build_query( array( 'findstr' => $args ) );

    return urldecode( $url );
  }

  /**
   * Create a pagination array.
   * Mainly used for php rendering.
   * If a modification is needed here, you also need to modify the javascript pagination function.
   * front/findstr-search/src/fields/pagination.js
   *
   *
   * @return array
   */
  public function pagination( $url ): array {

    $results = $this->results;

    if ( ! isset( $results['page'] ) ) {
      return array();
    }

    $current = $results['page'];
    $last    = $results['totalPages'];

    $delta           = 2;
    $left            = $current - $delta;
    $right           = $current + $delta + 1;
    $range           = array();
    $range_with_dots = array();
    $l               = null;
    /* translators: %s is current page */
    $current_label = _x( 'Page %d (current)', 'pagination label', 'findstr' );
    /* translators: %s is current page */
    $page_label = _x( 'Page %d', 'pagination label', 'findstr' );

    for ( $i = 1; $i <= $last; $i++ ) {
      if ( 1 === $i || $i === $last || ( $i >= $left && $i < $right ) ) {
          $label       = $i === $current ? $current_label : $page_label;
          $url['page'] = $i;

          $range[] = array(
            'page'    => $i,
            'current' => $i === $current,
            'label'   => sprintf( $label, $i ),
            'url'     => $this->build_url( $url ),
          );
      }
    }

    foreach ( $range as $item ) {
        $page        = $item['page'];
        $current     = $item['current'];
        $label       = $item['label'];
        $url['page'] = $page;

      if ( $l ) {
        if ( 2 === $page - $l ) {
          $range_with_dots[] = array(
            'page'    => $l + 1,
            'current' => $current,
            'label'   => $label,
            'url'     => $this->build_url( $url ),
          );
        } elseif ( 1 !== $page - $l ) {
            $range_with_dots[] = array(
              'page'       => '...',
              'current'    => $current,
              'isEllipsis' => true,
              'label'      => _x( 'Ellipsis', 'pagination label', 'findstr' ),
            );
        }
      }
        $range_with_dots[] = array(
          'page'    => $page,
          'current' => $current,
          'label'   => $label,
          'url'     => $this->build_url( $url ),
        );
        $l                 = $page;
    }

      return array(
        'pages'         => $range_with_dots,
        'isFirstPage'   => 1 === $results['page'],
        'isLastPage'    => $results['page'] === $results['totalPages'],
        'nextLabel'     => _x( 'Next page', 'pagination label', 'findstr' ),
        'previousLabel' => _x( 'Previous page', 'pagination label', 'findstr' ),
      );
  }

  public function search_results_permalinks( $results ) {
    $results['hits'] = array_map(
      function ( $hit ) {
        if ( is_array( $hit['language'] ) && ! empty( $hit['permalinks'] ) ) {
          $hit['permalink'] = $hit['permalinks'][ Helpers::get_language_code() ];
        }
        return $hit;
      },
      $results['hits']
    );

    return $results;
  }


}