<?php
/** 
 * @package     VikRestaurants
 * @subpackage  core
 * @author      E4J s.r.l.
 * @copyright   Copyright (C) 2023 E4J s.r.l. All Rights Reserved.
 * @license     http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
 * @link        https://vikwp.com
 */

namespace E4J\VikRestaurants\Upselling;

// No direct access
defined('ABSPATH') or die('No script kiddies please!');

/**
 * Upselling facade.
 * 
 * @since 1.11
 */
class UpsellingEngine
{
    /** @var ScoreCalculator */
    protected $scoreCalculator;

    /**
     * A configuration array.
     * 
     * @var array
     */
    protected $options;

    /**
     * A list of available take-away menus (IDs).
     * 
     * @var int[]|null
     */
    protected $availableMenus = null;

    /**
     * Class constructor.
     * 
     * @param  ScoreCalculator  $scoreCalculator
     * @param  array            $options
     */
    public function __construct(ScoreCalculator $scoreCalculator, array $options = [])
    {
        $this->scoreCalculator = $scoreCalculator;
        $this->options = $options;
    }

    /**
     * Returns the best upselling suggestions depending on the specified items
     * 
     * @param   int[]  $items  The ID of the items in cart.
     * @param   int    $topN   The maximum number of suggestions.
     * 
     * @return  object[]  The suggested items.
     */
    public function getSuggestions(array $items, int $topN = 3)
    {
        if (!$items)
        {
            // no items provided, immediately abort
            return [];
        }

        /** @var object[] */
        $sellables = $this->getItems($items);

        // iterate all the items in cart
        foreach ($items as $itemA)
        {
            // iterate all the sellable items 
            foreach ($sellables as $itemB)
            {
                // compute confidence score
                $score = $this->scoreCalculator->computeScore($itemA, $itemB->id);

                // update score if the new one is higher
                $itemB->score = max($score, $itemB->score);
            }
        }

        // get rid of items without confidence
        $sellables = array_filter($sellables, function($item) {
            return $item->score > 0;
        });

        // sort the sellable items by descending score
        usort($sellables, function($a, $b) {
            return $b->score <=> $a->score;
        });

        // take only the top-N elements
        return array_slice($sellables, 0, $topN);
    }

    /**
     * Returns a list with all the sellable items.
     * 
     * @param   int[]     $excluded  Ignore the specified items (IDs).
     * 
     * @return  object[]  A list of items.
     */
    protected function getItems(array $excluded = [])
    {
        $db = \JFactory::getDbo();

        $items = [];

        // pull all the published items
        $query = $db->getQuery(true)
            ->select($db->qn('e.id'))
            ->select($db->qn('e.id_takeaway_menu', 'idMenu'))
            ->from($db->qn('#__vikrestaurants_takeaway_menus_entry', 'e'))
            ->where($db->qn('e.published') . ' = 1');

        if ($excluded)
        {
            // exclude the specified items
            $query->where($db->qn('e.id') . ' NOT IN (' . implode(',', array_map('intval', $excluded)) . ')');
        }

        $db->setQuery($query);
        
        foreach ($db->loadObjectList() as $item)
        {
            $item->score = -INF;

            $items[] = $item;
        }

        /** @var int[]|null */
        $menus = $this->getAvailableMenus();

        // preserve only the items that belong to available menus
        $items = array_filter($items, function($item) use ($menus) {
            // check whether we should filter the products by available menu or
            // in case we are not looking for a specified booking date
            return $menus === null || in_array($item->idMenu, $menus);
        });

        /**
         * @todo should we also preserve only the items actually available in stock?
         */

        // reset keys
        return array_values($items);
    }

    /**
     * Returns all the available menus for the configured date.
     * 
     * @return  int[]|null  A list of available menus. Null in case the date hasn't been specified.
     */
    protected function getAvailableMenus()
    {
        if ($this->availableMenus === null && !empty($this->options['date']))
        {
            // pull all the available menus (only if the date has been provided)
            $this->availableMenus = \VikRestaurants::getAllTakeawayMenusOn([
                'date' => $this->options['date'],
                'hourmin' => $this->options['hourmin'] ?? null,
            ]);
        }

        return $this->availableMenus;
    }
}
