<?php
// https://www.drupal.org/docs/8/modules/search-api/developer-documentation/executing-a-search-in-code

namespace Drupal\materio_sapi\Controller;

use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\Component\Utility\Tags;
use Drupal\Component\Utility\Unicode;
use Drupal\search_api\Entity\Index;

// https://drupal.stackexchange.com/questions/225008/programatically-use-search-api

/**
 * Defines a route controller for materio sapi search (regular and ajax).
 */
class Base extends ControllerBase {



  private $limit = 15;
  private $offset = 0;

  private function sapiQuery(){
    // https://www.drupal.org/docs/8/modules/search-api/developer-documentation/executing-a-search-in-code
    // https://www.hashbangcode.com/article/drupal-8-date-search-boosting-search-api-and-solr-search
    // https://kgaut.net/blog/2018/drupal-8-search-api-effectuer-une-requete-dans-le-code.html


    // Otherwise, if you use Solr, you can also set the parse mode to “Direct query” 
    // and use something like 'tm_name:alex AND tm_surname:john' as the keywords (field names may vary). 
    // That will also ensure scoring works for the keywords (the other way just adds filters, which don’t affect scoring).
    // https://www.drupal.org/project/search_api/issues/3049097



    $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();

    $this->index = Index::load('database');

    $this->results = [
      'uuids' => [],
      'nids' => []
    ];
                                                                               
                                                                               
                                                                               
                                                                               
                                                                               
                                                                               
                                                                               
                                                                  
    // ,---.|                            ,---.                    
    // |---'|---.,---.,---.,---.,---.    |   |.   .,---.,---.,   .
    // |    |   ||    ,---|`---.|---'    |   ||   ||---'|    |   |
    // `    `   '`    `---^`---'`---'    `---\`---'`---'`    `---|
    //                                                       `---'
    // (to match exact materials names (like "wood-skin"))
    $this->phrase_query = $this->index->query(['offset'=>0,'limit'=>10000]);
    // set parse mode and conjunction
    $parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
    ->createInstance('phrase');
    $parse_mode->setConjunction('AND');
    $this->phrase_query->setParseMode($parse_mode);
    // Set fulltext search keywords and fields.
    if ($this->keys) {
      $this->phrase_query->keys(implode(' ', $this->keys));
    }
    $this->phrase_query->setFulltextFields(['title']);
    
    // Restrict the search to specific languages.
    $this->phrase_query->setLanguages([$lang]);

    // Do paging.
    // $this->and_query->range($this->offset, $this->limit);
    // retrieve all results
    // $this->and_query->range(0, -1);

    // Add sorting.
    $this->phrase_query->sort('search_api_relevance', 'DESC');

    // Set one or more tags for the query.
    // @see hook_search_api_query_TAG_alter()
    // @see hook_search_api_results_TAG_alter()
    $this->phrase_query->addTag('materio_sapi_search_phrase_query');

    $phrase_results = $this->phrase_query->execute();

    foreach ($phrase_results as $result) {
      $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
      $this->results['nids'][] = $result->getField('nid')->getValues()[0];
    }


                                                    
    // ,---.         |    ,---.                    
    // |---|,---.,---|    |   |.   .,---.,---.,   .
    // |   ||   ||   |    |   ||   ||---'|    |   |
    // `   '`   '`---'    `---\`---'`---'`    `---|
    //                                       `---'
    $this->and_query = $this->index->query(['offset'=>0,'limit'=>10000]);
    // set parse mode and conjunction
    $parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
    ->createInstance('terms');
    $parse_mode->setConjunction('AND');
    $this->and_query->setParseMode($parse_mode);
    // Set fulltext search keywords and fields.
    if ($this->keys) {
      $this->and_query->keys(implode(' ', $this->keys));
    }
    
    // in case of term id provided restrict the keys to taxo fields
    if ($this->terms && is_array($this->terms) && count($this->terms)) {
      $term_conditions = $this->and_query->createConditionGroup('OR');
      // $term = (int) $this->term;
      foreach ($this->terms as $term) {
        $tid = $term->value;
        foreach (['tag_tid', 'thesaurus_tid'] as $field) {
          $term_conditions->addCondition($field, (int) $tid);
        }
      }
      $this->and_query->addConditionGroup($term_conditions);

      // INSTEAD TRY TO BOOST THE TAG AND THESAURUS FIELDS
      // foreach ($taxoSolrFieldsName as $fname) {
      //   // $solarium_query->addParam('bf', "recip(abs(ms(NOW,{$solrField})),3.16e-11,10,0.1)");
      //   $bfparam = "if(gt(termfreq({$fname},{$this->term}),0),^21,0)";
      //   $this->or_query->addParam('bf', $bfparam);
      // }
      // look @ materio_sapi_search_api_solr_query_alter in materio_sapi.module
      // $this->or_query->setOption('termid', $this->term);
    }
    
    if ($this->filters) {
      // FILTERS
      $filters_conditions = $this->and_query->createConditionGroup('AND');
      foreach ($this->filters as $filter) {
        $filter = (int) $filter;
        foreach (['thesaurus_tid'] as $field) { // 'tag_tid', 
          $filters_conditions->addCondition($field, $filter);
        }
      }
      $this->and_query->addConditionGroup($filters_conditions);

      if(!$this->keys) {
        // if no keys but filters switch query to direct and add wildcard solr keys *:* 
        $direct_and_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
        ->createInstance('direct');
        $direct_and_parse_mode->setConjunction('AND');
        $this->and_query->setParseMode($direct_and_parse_mode);
        // $this->and_query->keys('*:*');
      }
    }
    // else{    
    $fulltextFields = [];
    // Recherche uniquement sur les champ thésaurus et tag
    $fulltextFields += [
      'thesaurus_name_0',
      'thesaurus_synonyms_0',
      'thesaurus_name_1',
      'thesaurus_synonyms_1',
      'thesaurus_name_2',
      'thesaurus_synonyms_2',
      'thesaurus_name_3',
      'thesaurus_synonyms_3',
      'thesaurus_name_4',
      'thesaurus_synonyms_4',
      'thesaurus_name',
      'thesaurus_synonyms',
      'tag_name',
      'tag_synonyms'
    ];
    if(count($fulltextFields)){
      $this->and_query->setFulltextFields($fulltextFields);
    }
    // }

    // Restrict the search to specific languages.
    $this->and_query->setLanguages([$lang]);

    // Do paging.
    // $this->and_query->range($this->offset, $this->limit);
    // retrieve all results
    // $this->and_query->range(0, -1);

    // Add sorting.
    $this->and_query->sort('search_api_relevance', 'DESC');

    // Set one or more tags for the query.
    // @see hook_search_api_query_TAG_alter()
    // @see hook_search_api_results_TAG_alter()
    $this->and_query->addTag('materio_sapi_search_and_query');

    $and_results = $this->and_query->execute();

    foreach ($and_results as $result) {
      // !! have to remove duplicates from phrase query
      $nid = $result->getField('nid')->getValues()[0];
      if ( !in_array($nid, $this->results['nids']) ) {
        $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
        $this->results['nids'][] = $result->getField('nid')->getValues()[0];
      }
    }


    $this->exactematch_count = count($this->results['nids']);


                                              
    // ,---.         ,---.                    
    // |   |,---.    |   |.   .,---.,---.,   .
    // |   ||        |   ||   ||---'|    |   |
    // `---'`        `---\`---'`---'`    `---|
    //                                   `---'
    $this->or_query = $this->index->query(['offset'=>0,'limit'=>10000]);

    // Change the parse mode for the search.
    // Les différentes possibilités sont
    //  - « direct » => Requête directe
    //  - « terms » => Multiple words
    //  - « phrase » => Single phrase
    //  - " edismax " => ???
    $or_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
      ->createInstance('direct');
    $or_parse_mode->setConjunction('OR');
    $this->or_query->setParseMode($or_parse_mode);

    // Set fulltext search keywords and fields.
    if ($this->keys) {
      $this->or_query->keys(implode(' ', $this->keys));
    }

    // // exclude results from previous and_query
    // !! trigering solr "too many boolean clauses" error
    // $exclude_and_results_conditions = $this->or_query->createConditionGroup('AND');
    // foreach ($this->results['nids'] as $nid) {
    //   $exclude_and_results_conditions->addCondition('nid', $nid, '<>');
    // }
    // $this->or_query->addConditionGroup($exclude_and_results_conditions);

    if (preg_match_all('/[WTRPCMFGSO]\d{4}/i', implode(' ', $this->keys), $matches)) {
      // in case we search for material references like W0117
      $ref_conditions = $this->or_query->createConditionGroup('OR');
      foreach ($matches[0] as $key => $value) {
        $ref_conditions->addCondition('field_reference', $value);
      }
      $this->or_query->addConditionGroup($ref_conditions);
    }

    if ($this->filters) {
      // FILTERS
      $or_filters_conditions = $this->or_query->createConditionGroup('OR');
      foreach ($this->filters as $filter) {
        $filter = (int) $filter;
        foreach (['thesaurus_tid'] as $field) { // 'tag_tid', 
          $or_filters_conditions->addCondition($field, $filter);
        }
      }
      $this->or_query->addConditionGroup($or_filters_conditions);

      if(!$this->keys) {
        // if no keys but filters switch query to direct and add wildcard solr keys *:* 
        $direct_or_parse_mode = \Drupal::service('plugin.manager.search_api.parse_mode')
        ->createInstance('direct');
        $direct_or_parse_mode->setConjunction('OR');
        $this->or_query->setParseMode($direct_or_parse_mode);
        // $this->or_query->keys('*:*');
      }

    }

    // Restrict the search to specific languages.
    $this->or_query->setLanguages([$lang]);

    // Do paging.
    // $this->or_query->range($this->offset, $this->limit);
    // retrieve all results
    // $this->or_query->range(0, -1);

    // Add sorting.
    $this->or_query->sort('search_api_relevance', 'DESC');

    // Set one or more tags for the query.
    // @see hook_search_api_query_TAG_alter()
    // @see hook_search_api_results_TAG_alter()
    $this->or_query->addTag('materio_sapi_search_or_query');

    $or_results = $this->or_query->execute();

    foreach ($or_results as $result) {
      $nid = $result->getField('nid')->getValues()[0];
      // !! have to remove duplicates instead of $exclude_and_results_conditions (solr too many boolean clauses)
      if ( !in_array($nid, $this->results['nids']) ) {
        $this->results['uuids'][] = $result->getField('uuid')->getValues()[0];
        $this->results['nids'][] = $nid;
      }
    }

    // todo you may like / more like this

  }

  private function defaultQuery(){
    $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
    $entity_storage = \Drupal::entityTypeManager()->getStorage('node');
    $this->query = $entity_storage->getQuery()
      ->condition('type', ['materiau', 'thematique'], 'IN')
      ->condition('status', '1')
      ->condition('langcode', $lang)
      ->range($this->offset, $this->limit)
      ->accessCheck(TRUE)
      ->sort('created', 'DESC');
      // ->condition('field_example', 'test_value')
    $this->results = $this->query->execute();

    $this->count_query = $entity_storage->getQuery()
      ->condition('type', ['materiau', 'thematique'], 'IN')
      ->condition('langcode', $lang)
      ->accessCheck(TRUE)
      ->condition('status', '1')
      ->count();
    $this->count = $this->count_query->execute();
  }

  /**
   * get params from request
   */
  private function parseRequest(Request $request){
    // Get the typed string from the URL, if it exists.
    $this->keys = $request->query->get('keys');
    if($this->keys){
      $this->keys = mb_strtolower($this->keys);
      $this->keys = Tags::explode($this->keys);
      // \Drupal::logger('materio_sapi')->notice($this->keys);
    }
    // get the exacte term id in case of autocomplete
    // $this->terms = $request->query->get('terms');
    $t = $request->query->get('terms');
    // $this->terms = strlen($t) ? explode(',', $t) : null;
    $this->terms = strlen($t) ? json_decode($t) : null;

    // get the filters of advanced search
    $f = $request->query->get('filters');
    $this->filters = strlen($f) ? explode(',', $f) : null;
    // $this->allparams = $request->query->all();
    // $request->attributes->get('_raw_variables')->get('filters')
    //
    $this->offset = $request->query->get('offset') ?? $this->offset;
    $this->limit = $request->query->get('limit') ?? $this->limit;
  }

  /**
   * Handler for ajax search.
   */
  public function getResults(Request $request) {
    $this->parseRequest($request);

    $resp = [
      'range' => array(
        'offset' => $this->offset,
        'limit' => $this->limit
      ),
    ];

    if ($this->keys || $this->filters) {
      $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();

      $this->sapiQuery();

      $resp['keys'] = json_encode($this->keys);
      $resp['terms'] = json_encode($this->terms);
      $resp['filters'] = $this->filters;
      // $resp['count'] = $this->results->getResultCount();
      $resp['count'] = count($this->results['nids']);
      $resp['exactematch_count'] = $this->exactematch_count;
      $resp['infos'] = t('The search found @exactmatchcount exact match result(s) for @count total result(s) with', array(
        "@exactmatchcount" => $resp['exactematch_count'],
        "@count" => $resp['count']
      ));
      if ($this->keys) {
        $resp['infos'] .= t(' keywords @keys', array(
          "@keys" => implode(', ', $this->keys)
        ));
      }
      if ($this->keys && $this->filters) {
        $resp['infos'] .= " and";
      }
      if ($this->filters) {
        // get the terms names from tid
        $storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
        $filters_names = [];
        foreach ($this->filters as $tid) {
          /** @var \Drupal\Core\Entity\EntityInterface $entity */
          $entity = $storage->load($tid);
          $entity_trans = \Drupal::service('entity.repository')->getTranslationFromContext($entity, $lang);
          $filters_names[] = $entity_trans->getName();
        }
        $resp['infos'] .= t(' filters @filters', array(
          "@filters" => implode(', ', $filters_names)
        ));
      }
      // $resp['options'] = $this->query->getOptions();

      // $uuids = [];
      // $nids = [];
      // foreach ($this->results as $result) {
      //   $uuids[] = $result->getField('uuid')->getValues()[0];
      //   $nids[] = $result->getField('nid')->getValues()[0];
      // }
      $resp['nids'] = array_slice($this->results['nids'], $this->offset, $this->limit);
      $resp['uuids'] = array_slice($this->results['uuids'], $this->offset, $this->limit);
    } else {
      // no keys or terms to search for
      // display the default base page
      $this->defaultQuery();
      // $uuids = [];
      $nids = [];
      // Using entityTypeManager
      // Get a node storage object.
      // $node_storage = \Drupal::entityTypeManager()->getStorage('node');

      foreach ($this->results as $result) {
        // $lang = \Drupal::languageManager()->getCurrentLanguage()->getId();
        // Load a single node.
        // $node = $node_storage->load($result);
        // check if has translation
        // i used to filter like bellow because of a graphql probleme
        // if ($node->hasTranslation($lang)) {
          $nids[] = $result;
        // }

      }
      // $resp['uuids'] = $uuids;
      $resp['nids'] = $nids;
      $resp['count'] = $this->count;
      $resp['infos'] = t('Please use the search form to search from our @count materials.', array(
        "@count" => $resp['count']
      ));
    }

    return new JsonResponse($resp);
  }


  /**
   * Handler for regular page search.
   */
  function pageHandler(Request $request){
    // \Drupal::logger('materio_sapi')->notice(print_r($request, true));

    $this->parseRequest($request);

    $resp = [
      "#title" => 'Base'
    ];

    if ($this->keys) {
      $resp['#title'] = implode(', ', $this->keys);

      // $this->sapiQuery();

      // $node_storage = \Drupal::entityTypeManager()->getStorage('node');
      // $node_view_builder = \Drupal::entityTypeManager()->getViewBuilder('node');

      // // $items = $this->results->getResultItems();
      // $nids = $this->results['nids'];
      // $this->items = [];
      // foreach ($nids as $nid) {
      //   // \Drupal::logger('materio_sapi')->notice(print_r($nid, true));
      //   try {
      //     /** @var \Drupal\Core\Entity\EntityInterface $entity */
      //     // $entity = $item->getOriginalObject()->getValue();
      //     $entity = $node_storage->load($nid);
      //   }
      //   catch (SearchApiException $e) {
      //     continue;
      //   }
      //   if (!$entity) {
      //     continue;
      //   }
      //   // TODO: define dynamicly viewmode
      //   $this->items[] = $node_view_builder->view($entity, 'teaser');

      // }

      // $resp['items'] = $this->items;
      $resp['items'] = [];
    }else{
      $resp['#markup'] = t("no keys to search for");
    }

    return $resp;
  }
}
