Covivo/mobicoop

View on GitHub
api/src/Carpool/Service/ProposalMatcher.php

Summary

Maintainability
F
1 mo
Test Coverage
<?php

/**
 * Copyright (c) 2018, MOBICOOP. All rights reserved.
 * This project is dual licensed under AGPL and proprietary licence.
 ***************************
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as
 *    published by the Free Software Foundation, either version 3 of the
 *    License, or (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <gnu.org/licenses>.
 ***************************
 *    Licence MOBICOOP described in the file
 *    LICENSE
 */

namespace App\Carpool\Service;

use App\Carpool\Entity\Ask;
use App\Carpool\Entity\Criteria;
use App\Carpool\Entity\Matching;
use App\Carpool\Entity\Proposal;
use App\Carpool\Entity\Waypoint;
use App\Carpool\Repository\ProposalRepository;
use App\Geography\Entity\Address;
use App\Geography\Interfaces\GeorouterInterface;
use App\Geography\Service\GeoRouter;
use App\Import\Entity\UserImport;
use App\Match\Entity\Candidate;
use App\Match\Service\GeoMatcher;
use App\Service\FormatDataManager;
use App\User\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * Matching analyzer service.
 *
 * @author Sylvain Briat <sylvain.briat@covivo.eu>
 */
class ProposalMatcher
{
    // behaviour in case of multiple matches for the same candidates
    // 1 = keep fastest route
    // 2 = keep shortest route
    // 3 = keep all routes
    public const MULTI_MATCHES_FOR_SAME_CANDIDATES = self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST;
    public const MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST = 1;
    public const MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST = 2;
    public const MULTI_MATCHES_FOR_SAME_CANDIDATES_ALL = 3;
    // TODO : should depend on the total distance : total distance => max detour allowed
    // Maximum percentage distance detour for the driver to pickup and drop off the passenger
    private static $maxDetourDistancePercent;
    // Maximum percentage duration detour for the driver to pickup and drop off the passenger
    private static $maxDetourDurationPercent;

    // Minimum driver's trip distance (in km) to check the common distance percentage
    private static $minCommonDistanceCheck;
    // Minimum common distance accepted for the passenger's journey relative to the driver's journey distance
    private static $minCommonDistancePercent;

    private $entityManager;
    private $proposalRepository;
    private $geoMatcher;
    private $geoRouter;
    private $logger;
    private $formatDataManager;
    private $params;

    /**
     * Constructor.
     */
    public function __construct(EntityManagerInterface $entityManager, ProposalRepository $proposalRepository, GeoMatcher $geoMatcher, GeoRouter $geoRouter, LoggerInterface $logger, FormatDataManager $formatDataManager, array $params)
    {
        $this->entityManager = $entityManager;
        $this->proposalRepository = $proposalRepository;
        $this->geoRouter = $geoRouter;
        $this->geoMatcher = $geoMatcher;
        $this->logger = $logger;
        $this->formatDataManager = $formatDataManager;
        $this->params = $params;
        static::$maxDetourDistancePercent = $params['maxDetourDistancePercent'];
        static::$maxDetourDurationPercent = $params['maxDetourDurationPercent'];
        static::$minCommonDistanceCheck = $params['minCommonDistanceCheck'];
        static::$minCommonDistancePercent = $params['minCommonDistancePercent'];
    }

    /**
     * Create Matching proposal entities for a proposal.
     *
     * @param Proposal $proposal            The proposal for which we want the matchings
     * @param bool     $excludeProposalUser Exclude the matching proposals made by the proposal user
     *
     * @return Proposal The proposal with the matchings
     */
    public function createMatchingsForProposal(Proposal $proposal, bool $excludeProposalUser = true)
    {
        // $this->logger->info("ProposalMatcher : createMatchingsForProposal " . (new \DateTime("UTC"))->format("Ymd H:i:s.u"));

        set_time_limit(360);

        // we search the matchings
        $matchings = $this->findMatchingProposals($proposal, $excludeProposalUser);

        // we assign the matchings to the proposal
        foreach ($matchings as $matching) {
            if ($matching->getProposalOffer() === $proposal) {
                $proposal->addMatchingRequest($matching);
            } else {
                $proposal->addMatchingOffer($matching);
            }
        }

        return $proposal;
    }

    /**
     * Get the matching filters.
     *
     * @param Matching $matching The matching
     *
     * @return array The matching return filters
     */
    public function getMatchingFilters(Matching $matching)
    {
        $filters = [];
        $candidateDriver = new Candidate();
        $candidateDriver->setId(!is_null($matching->getProposalOffer()->getUser()) ? $matching->getProposalOffer()->getUser()->getId() : User::DEFAULT_ID);
        $addresses = [];
        foreach ($matching->getProposalOffer()->getWaypoints() as $waypoint) {
            $addresses[] = $waypoint->getAddress();
        }
        $candidateDriver->setAddresses($addresses);
        // we compute the driver's direction
        if ($routes = $this->geoRouter->getRoutes($addresses, true, false, GeorouterInterface::RETURN_TYPE_OBJECT)) {
            $direction = $routes[0];
            $candidateDriver->setDirection($direction);
            $candidateDriver->setMaxDetourDistance($direction->getDistance() * static::$maxDetourDistancePercent / 100);
            $candidateDriver->setMaxDetourDuration($direction->getDuration() * static::$maxDetourDurationPercent / 100);
        }

        $candidatePassenger = new Candidate();
        $candidatePassenger->setId(!is_null($matching->getProposalRequest()->getUser()) ? $matching->getProposalRequest()->getUser()->getId() : User::DEFAULT_ID);
        $addressesCandidate = [];
        foreach ($matching->getProposalRequest()->getWaypoints() as $waypoint) {
            $addressesCandidate[] = $waypoint->getAddress();
        }
        $candidatePassenger->setAddresses($addressesCandidate);
        if ($routes = $this->geoRouter->getRoutes([$addressesCandidate[0], $addressesCandidate[count($addressesCandidate) - 1]], true, false, GeorouterInterface::RETURN_TYPE_OBJECT)) {
            $candidatePassenger->setDirection($routes[0]);
        }
        if ($matches = $this->geoMatcher->forceMatch($candidateDriver, $candidatePassenger)) {
            // many matches can be found for 2 candidates : if multiple routes satisfy the criteria
            if (is_array($matches) && count($matches) > 0) {
                switch (self::MULTI_MATCHES_FOR_SAME_CANDIDATES) {
                    case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST:
                        usort($matches, self::build_sorter('newDuration'));
                        $filters = $matches[0];

                        break;

                    case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST:
                        usort($matches, self::build_sorter('newDistance'));
                        $filters = $matches[0];

                        break;

                    default:
                        $filters = $matches[0];

                        break;
                }
            }
        }

        return $filters;
    }

    /**
     * Get the return ask filters.
     *
     * @param Ask $ask The ask
     *
     * @return array The ask return filters
     */
    public function getAskFilters(Ask $ask)
    {
        $filters = [];
        $candidateDriver = new Candidate();
        $candidateDriver->setId($ask->getMatching()->getProposalOffer()->getUser()->getId());
        $addresses = [];
        foreach ($ask->getMatching()->getProposalOffer()->getWaypoints() as $waypoint) {
            if ($waypoint->isFloating()) {
                continue;
            }
            $addresses[] = $waypoint->getAddress();
        }
        $candidateDriver->setAddresses($addresses);
        // we compute the driver's direction
        if ($routes = $this->geoRouter->getRoutes($addresses)) {
            $direction = $routes[0];
            $candidateDriver->setDirection($direction);
            $candidateDriver->setMaxDetourDistance($direction->getDistance() * static::$maxDetourDistancePercent / 100);
            $candidateDriver->setMaxDetourDuration($direction->getDuration() * static::$maxDetourDurationPercent / 100);
        }

        $candidatePassenger = new Candidate();
        $candidatePassenger->setId($ask->getMatching()->getProposalRequest()->getUser()->getId());
        $addressesCandidate = [];
        foreach ($ask->getMatching()->getProposalRequest()->getWaypoints() as $waypoint) {
            if ($waypoint->isFloating()) {
                continue;
            }
            $addressesCandidate[] = $waypoint->getAddress();
        }
        $candidatePassenger->setAddresses($addressesCandidate);
        if ($routes = $this->geoRouter->getRoutes([$addressesCandidate[0], $addressesCandidate[count($addressesCandidate) - 1]])) {
            $candidatePassenger->setDirection($routes[0]);
        }
        if ($matches = $this->geoMatcher->forceMatch($candidateDriver, $candidatePassenger)) {
            // many matches can be found for 2 candidates : if multiple routes satisfy the criteria
            if (is_array($matches) && count($matches) > 0) {
                switch (self::MULTI_MATCHES_FOR_SAME_CANDIDATES) {
                    case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST:
                        usort($matches, self::build_sorter('newDuration'));
                        $filters = $matches[0];

                        break;

                    case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST:
                        usort($matches, self::build_sorter('newDistance'));
                        $filters = $matches[0];

                        break;

                    default:
                        $filters = $matches[0];

                        break;
                }
            }
        }

        return $filters;
    }

    /**
     * Find matching proposals for a proposal.
     * Important note : we use arrays instead of Proposal objects to speed up the process.
     * Returns an array of Matching objects.
     *
     * @param bool $excludeProposalUser Exclude the matching proposals made by the proposal user
     *
     * @return null|array
     */
    public function findMatchingProposals(Proposal $proposal, bool $excludeProposalUser = true)
    {
        // $this->logger->info("ProposalMatcher : findMatchingProposals " . (new \DateTime("UTC"))->format("Ymd H:i:s.u"));

        $this->entityManager->persist($proposal);
        $this->entityManager->flush();

        // we search matching proposals in the database
        // if no proposals are found we return an empty array
        // $this->logger->info("ProposalMatcher : proposalRepository findMatchingProposals " . (new \DateTime("UTC"))->format("Ymd H:i:s.u"));
        if (!$proposalsFound = $this->proposalRepository->findMatchingProposals($proposal, $excludeProposalUser)) {
            return [];
        }
        // echo count($proposalsFound);die;

        $this->logger->info('ProposalMatcher : create proposals for '.count($proposalsFound).' results '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        $proposals = [];
        foreach ($proposalsFound as $key => $proposalFound) {
            if (!array_key_exists($proposalFound['pid'], $proposals)) {
                $proposals[$proposalFound['pid']] = [
                    'pid' => $proposalFound['pid'],
                    'uid' => $proposalFound['uid'],
                    'driver' => $proposalFound['driver'],
                    'passenger' => $proposalFound['passenger'],
                    'maxDetourDuration' => $proposalFound['maxDetourDuration'],
                    'maxDetourDistance' => $proposalFound['maxDetourDistance'],
                    'dpduration' => $proposalFound['dpduration'],
                    'dpdistance' => $proposalFound['dpdistance'],
                    'ddduration' => $proposalFound['ddduration'],
                    'dddistance' => $proposalFound['dddistance'],
                    'addresses' => [
                        [
                            'id' => $proposalFound['wid'],
                            'position' => $proposalFound['position'],
                            'destination' => $proposalFound['destination'],
                            'reached' => $proposalFound['reached'],
                            'latitude' => $proposalFound['latitude'],
                            'longitude' => $proposalFound['longitude'],
                            'streetAddress' => $proposalFound['streetAddress'],
                            'postalCode' => $proposalFound['postalCode'],
                            'addressLocality' => $proposalFound['addressLocality'],
                            'addressCountry' => $proposalFound['addressCountry'],
                            'elevation' => $proposalFound['elevation'],
                            'houseNumber' => $proposalFound['houseNumber'],
                            'street' => $proposalFound['street'],
                            'subLocality' => $proposalFound['subLocality'],
                            'localAdmin' => $proposalFound['localAdmin'],
                            'county' => $proposalFound['county'],
                            'macroCounty' => $proposalFound['macroCounty'],
                            'region' => $proposalFound['region'],
                            'macroRegion' => $proposalFound['macroRegion'],
                            'countryCode' => $proposalFound['countryCode'],
                            'name' => $proposalFound['addressName'],
                        ],
                    ],
                ];
            } else {
                $element = [
                    'id' => $proposalFound['wid'],
                    'position' => $proposalFound['position'],
                    'destination' => $proposalFound['destination'],
                    'reached' => $proposalFound['reached'],
                    'latitude' => $proposalFound['latitude'],
                    'longitude' => $proposalFound['longitude'],
                    'streetAddress' => $proposalFound['streetAddress'],
                    'postalCode' => $proposalFound['postalCode'],
                    'addressLocality' => $proposalFound['addressLocality'],
                    'addressCountry' => $proposalFound['addressCountry'],
                    'elevation' => $proposalFound['elevation'],
                    'houseNumber' => $proposalFound['houseNumber'],
                    'street' => $proposalFound['street'],
                    'subLocality' => $proposalFound['subLocality'],
                    'localAdmin' => $proposalFound['localAdmin'],
                    'county' => $proposalFound['county'],
                    'macroCounty' => $proposalFound['macroCounty'],
                    'region' => $proposalFound['region'],
                    'macroRegion' => $proposalFound['macroRegion'],
                    'countryCode' => $proposalFound['countryCode'],
                    'name' => $proposalFound['addressName'],
                ];
                if (!in_array($element, $proposals[$proposalFound['pid']]['addresses'])) {
                    $proposals[$proposalFound['pid']]['addresses'][] = $element;
                }
            }
        }
        ksort($proposals);
        // var_dump($proposals);exit;
        // echo count($proposals);die;
        // $this->logger->info('ProposalMatcher : created proposals : '.count($proposals).' '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        $matchings = [];

        // we filter with geomatcher
        $candidateProposal = new Candidate();
        if ($proposal->getUser()) {
            $candidateProposal->setId($proposal->getUser()->getId());
        } else {
            $candidateProposal->setId(User::DEFAULT_ID);
        }
        $addresses = [];
        foreach ($proposal->getWaypoints() as $waypoint) {
            if (!$waypoint->isReached()) {
                $addresses[] = $waypoint->getAddress();
            }
        }
        $candidateProposal->setAddresses($addresses);

        $pears = []; // list of proposals to test

        // $this->logger->info('ProposalMatcher : create pears as driver '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        if ($proposal->getCriteria()->isDriver()) {
            $cCandidateProposal = clone $candidateProposal;
            $cCandidateProposal->setMaxDetourDistance($proposal->getCriteria()->getMaxDetourDistance() ? $proposal->getCriteria()->getMaxDetourDistance() : ($proposal->getCriteria()->getDirectionDriver()->getDistance() * static::$maxDetourDistancePercent / 100));
            $cCandidateProposal->setMaxDetourDuration($proposal->getCriteria()->getMaxDetourDuration() ? $proposal->getCriteria()->getMaxDetourDuration() : ($proposal->getCriteria()->getDirectionDriver()->getDuration() * static::$maxDetourDurationPercent / 100));
            $cCandidateProposal->setDirection($proposal->getCriteria()->getDirectionDriver());
            $candidatesPassenger = [];
            foreach ($proposals as $proposalToMatch) {
                // if the candidate is not passenger we skip (the 2 candidates could be driver AND passenger, and the second one match only as a driver)
                if (!$proposalToMatch['passenger']) {
                    continue;
                }
                $candidate = new Candidate();
                $candidate->setId($proposalToMatch['pid']);
                $addressesCandidate = [];
                usort($proposalToMatch['addresses'], function ($a, $b) {
                    return $a['position'] <=> $b['position'];
                });
                foreach ($proposalToMatch['addresses'] as $waypoint) {
                    if (!$waypoint['reached']) {
                        $address = new Address();
                        $address->setLatitude($waypoint['latitude']);
                        $address->setLongitude($waypoint['longitude']);
                        $address->setStreetAddress($waypoint['streetAddress']);
                        $address->setPostalCode($waypoint['postalCode']);
                        $address->setAddressLocality($waypoint['addressLocality']);
                        $address->setAddressCountry($waypoint['addressCountry']);
                        $address->setElevation($waypoint['elevation']);
                        $address->setHouseNumber($waypoint['houseNumber']);
                        $address->setStreet($waypoint['street']);
                        $address->setSubLocality($waypoint['subLocality']);
                        $address->setLocalAdmin($waypoint['localAdmin']);
                        $address->setCounty($waypoint['county']);
                        $address->setMacroCounty($waypoint['macroCounty']);
                        $address->setRegion($waypoint['region']);
                        $address->setMacroRegion($waypoint['macroRegion']);
                        $address->setCountryCode($waypoint['countryCode']);
                        $address->setName($waypoint['name']);
                        $addressesCandidate[] = $address;
                    }
                }
                $candidate->setAddresses($addressesCandidate);
                $candidate->setDuration($proposalToMatch['dpduration']);
                $candidate->setDistance($proposalToMatch['dpdistance']);

                // the 2 following are not taken in account right now as only the driver detour matters
                $candidate->setMaxDetourDistance($proposalToMatch['maxDetourDistance'] ? $proposalToMatch['maxDetourDistance'] : ($proposalToMatch['dpdistance'] * static::$maxDetourDistancePercent / 100));
                $candidate->setMaxDetourDuration($proposalToMatch['maxDetourDuration'] ? $proposalToMatch['maxDetourDuration'] : ($proposalToMatch['dpduration'] * static::$maxDetourDurationPercent / 100));
                $candidatesPassenger[] = $candidate;
            }
            $pears[] = [
                'candidate' => $cCandidateProposal,
                'candidates' => $candidatesPassenger,
                'master' => true,
            ];
        }

        // $this->logger->info('ProposalMatcher : create pears as passenger '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        if ($proposal->getCriteria()->isPassenger()) {
            $cCandidateProposal = clone $candidateProposal;
            $cCandidateProposal->setDirection($proposal->getCriteria()->getDirectionPassenger());
            // the 2 following are not taken in account right now as only the driver detour matters
            $cCandidateProposal->setMaxDetourDistance($proposal->getCriteria()->getMaxDetourDistance() ? $proposal->getCriteria()->getMaxDetourDistance() : ($proposal->getCriteria()->getDirectionPassenger()->getDistance() * static::$maxDetourDistancePercent / 100));
            $cCandidateProposal->setMaxDetourDuration($proposal->getCriteria()->getMaxDetourDuration() ? $proposal->getCriteria()->getMaxDetourDuration() : ($proposal->getCriteria()->getDirectionPassenger()->getDuration() * static::$maxDetourDurationPercent / 100));
            $candidatesDriver = [];
            foreach ($proposals as $proposalToMatch) {
                // if the candidate is not driver we skip (the 2 candidates could be driver AND passenger, and the second one match only as a passenger)
                if (!$proposalToMatch['driver']) {
                    continue;
                }
                $candidate = new Candidate();
                $candidate->setId($proposalToMatch['pid']);
                $addressesCandidate = [];
                usort($proposalToMatch['addresses'], function ($a, $b) {
                    return $a['position'] <=> $b['position'];
                });
                foreach ($proposalToMatch['addresses'] as $waypoint) {
                    if (!$waypoint['reached']) {
                        $address = new Address();
                        $address->setLatitude($waypoint['latitude']);
                        $address->setLongitude($waypoint['longitude']);
                        $address->setStreetAddress($waypoint['streetAddress']);
                        $address->setPostalCode($waypoint['postalCode']);
                        $address->setAddressLocality($waypoint['addressLocality']);
                        $address->setAddressCountry($waypoint['addressCountry']);
                        $address->setElevation($waypoint['elevation']);
                        $address->setHouseNumber($waypoint['houseNumber']);
                        $address->setStreet($waypoint['street']);
                        $address->setSubLocality($waypoint['subLocality']);
                        $address->setLocalAdmin($waypoint['localAdmin']);
                        $address->setCounty($waypoint['county']);
                        $address->setMacroCounty($waypoint['macroCounty']);
                        $address->setRegion($waypoint['region']);
                        $address->setMacroRegion($waypoint['macroRegion']);
                        $address->setCountryCode($waypoint['countryCode']);
                        $address->setName($waypoint['name']);
                        $addressesCandidate[] = $address;
                    }
                }
                $candidate->setAddresses($addressesCandidate);
                $candidate->setDuration($proposalToMatch['ddduration']);
                $candidate->setDistance($proposalToMatch['dddistance']);
                $candidate->setMaxDetourDistance($proposalToMatch['maxDetourDistance'] ? $proposalToMatch['maxDetourDistance'] : ($proposalToMatch['dddistance'] * static::$maxDetourDistancePercent / 100));
                $candidate->setMaxDetourDuration($proposalToMatch['maxDetourDuration'] ? $proposalToMatch['maxDetourDuration'] : ($proposalToMatch['ddduration'] * static::$maxDetourDurationPercent / 100));
                $candidatesDriver[] = $candidate;
            }
            $pears[] = [
                'candidate' => $cCandidateProposal,
                'candidates' => $candidatesDriver,
                'master' => false,
            ];
        }

        // $this->logger->info('ProposalMatcher : single Match '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        if ($matches = $this->geoMatcher->singleMatch($pears)) {
            if (isset($matches['driver']) && is_array($matches['driver']) && count($matches['driver']) > 0) {
                // $this->logger->info('ProposalMatcher : single Match treat passengers '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                // there are matches as driver
                foreach ($matches['driver'] as $candidateId => $matchesDriver) {
                    // we sort each possible matches as many matches can be found for 2 candidates : if multiple routes satisfy the criteria
                    switch (self::MULTI_MATCHES_FOR_SAME_CANDIDATES) {
                        case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST:
                            usort($matchesDriver, self::build_sorter('newDuration'));
                            $matching = new Matching();
                            $matching->setProposalOffer($proposal);
                            $matching->setProposalRequest($this->proposalRepository->find($candidateId));
                            $matching->setFilters($matchesDriver[0]);
                            $matching->setOriginalDistance($matchesDriver[0]['originalDistance']);
                            $matching->setAcceptedDetourDistance($matchesDriver[0]['acceptedDetourDistance']);
                            $matching->setNewDistance($matchesDriver[0]['newDistance']);
                            $matching->setDetourDistance($matchesDriver[0]['detourDistance']);
                            $matching->setDetourDistancePercent($matchesDriver[0]['detourDistancePercent']);
                            $matching->setOriginalDuration($matchesDriver[0]['originalDuration']);
                            $matching->setAcceptedDetourDuration($matchesDriver[0]['acceptedDetourDuration']);
                            $matching->setNewDuration($matchesDriver[0]['newDuration']);
                            $matching->setDetourDuration($matchesDriver[0]['detourDuration']);
                            $matching->setDetourDurationPercent($matchesDriver[0]['detourDurationPercent']);
                            $matching->setCommonDistance($matchesDriver[0]['commonDistance']);
                            $matchings[] = $matching;

                            break;

                        case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST:
                            usort($matchesDriver, self::build_sorter('newDistance'));
                            $matching = new Matching();
                            $matching->setProposalOffer($proposal);
                            $matching->setProposalRequest($this->proposalRepository->find($candidateId));
                            $matching->setFilters($matchesDriver[0]);
                            $matching->setOriginalDistance($matchesDriver[0]['originalDistance']);
                            $matching->setAcceptedDetourDistance($matchesDriver[0]['acceptedDetourDistance']);
                            $matching->setNewDistance($matchesDriver[0]['newDistance']);
                            $matching->setDetourDistance($matchesDriver[0]['detourDistance']);
                            $matching->setDetourDistancePercent($matchesDriver[0]['detourDistancePercent']);
                            $matching->setOriginalDuration($matchesDriver[0]['originalDuration']);
                            $matching->setAcceptedDetourDuration($matchesDriver[0]['acceptedDetourDuration']);
                            $matching->setNewDuration($matchesDriver[0]['newDuration']);
                            $matching->setDetourDuration($matchesDriver[0]['detourDuration']);
                            $matching->setDetourDurationPercent($matchesDriver[0]['detourDurationPercent']);
                            $matching->setCommonDistance($matchesDriver[0]['commonDistance']);
                            $matchings[] = $matching;

                            break;

                        default:
                            break;
                    }
                }
            }
            if (isset($matches['passenger']) && is_array($matches['passenger']) && count($matches['passenger']) > 0) {
                // $this->logger->info('ProposalMatcher : single Match treat drivers '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                // there are matches as passenger
                foreach ($matches['passenger'] as $candidateId => $matchesPassenger) {
                    // we sort each possible matches as many matches can be found for 2 candidates : if multiple routes satisfy the criteria
                    switch (self::MULTI_MATCHES_FOR_SAME_CANDIDATES) {
                        case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST:
                            usort($matchesPassenger, self::build_sorter('newDuration'));
                            $matching = new Matching();
                            $matching->setProposalOffer($this->proposalRepository->find($candidateId));
                            $matching->setProposalRequest($proposal);
                            $matching->setFilters($matchesPassenger[0]);
                            $matching->setOriginalDistance($matchesPassenger[0]['originalDistance']);
                            $matching->setAcceptedDetourDistance($matchesPassenger[0]['acceptedDetourDistance']);
                            $matching->setNewDistance($matchesPassenger[0]['newDistance']);
                            $matching->setDetourDistance($matchesPassenger[0]['detourDistance']);
                            $matching->setDetourDistancePercent($matchesPassenger[0]['detourDistancePercent']);
                            $matching->setOriginalDuration($matchesPassenger[0]['originalDuration']);
                            $matching->setAcceptedDetourDuration($matchesPassenger[0]['acceptedDetourDuration']);
                            $matching->setNewDuration($matchesPassenger[0]['newDuration']);
                            $matching->setDetourDuration($matchesPassenger[0]['detourDuration']);
                            $matching->setDetourDurationPercent($matchesPassenger[0]['detourDurationPercent']);
                            $matching->setCommonDistance($matchesPassenger[0]['commonDistance']);
                            $matchings[] = $matching;

                            break;

                        case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST:
                            usort($matchesPassenger, self::build_sorter('newDistance'));
                            $matching = new Matching();
                            $matching->setProposalOffer($this->proposalRepository->find($candidateId));
                            $matching->setProposalRequest($proposal);
                            $matching->setFilters($matchesPassenger[0]);
                            $matching->setOriginalDistance($matchesPassenger[0]['originalDistance']);
                            $matching->setAcceptedDetourDistance($matchesPassenger[0]['acceptedDetourDistance']);
                            $matching->setNewDistance($matchesPassenger[0]['newDistance']);
                            $matching->setDetourDistance($matchesPassenger[0]['detourDistance']);
                            $matching->setDetourDistancePercent($matchesPassenger[0]['detourDistancePercent']);
                            $matching->setOriginalDuration($matchesPassenger[0]['originalDuration']);
                            $matching->setAcceptedDetourDuration($matchesPassenger[0]['acceptedDetourDuration']);
                            $matching->setNewDuration($matchesPassenger[0]['newDuration']);
                            $matching->setDetourDuration($matchesPassenger[0]['detourDuration']);
                            $matching->setDetourDurationPercent($matchesPassenger[0]['detourDurationPercent']);
                            $matching->setCommonDistance($matchesPassenger[0]['commonDistance']);
                            $matchings[] = $matching;

                            break;

                        default:
                            break;
                    }
                }
            }
        }
        // $this->logger->info('ProposalMatcher : nb of potential matchings : '.count($matchings).' | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        // $this->logger->info('ProposalMatcher : checkPickUp '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        // if we use times, we check if the pickup times match
        if (
            (Criteria::FREQUENCY_PUNCTUAL == $proposal->getCriteria()->getFrequency() && $proposal->getUseTime())
                || (Criteria::FREQUENCY_REGULAR == $proposal->getCriteria()->getFrequency() && (
                    ($proposal->getCriteria()->isMonCheck() && $proposal->getCriteria()->getMonTime())
                    || ($proposal->getCriteria()->isTueCheck() && $proposal->getCriteria()->getTueTime())
                    || ($proposal->getCriteria()->isWedCheck() && $proposal->getCriteria()->getWedTime())
                    || ($proposal->getCriteria()->isThuCheck() && $proposal->getCriteria()->getThuTime())
                    || ($proposal->getCriteria()->isFriCheck() && $proposal->getCriteria()->getFriTime())
                    || ($proposal->getCriteria()->isSatCheck() && $proposal->getCriteria()->getSatTime())
                    || ($proposal->getCriteria()->isSunCheck() && $proposal->getCriteria()->getSunTime())
                ))
        ) {
            $matchings = $this->checkPickUp($matchings);
        }
        // $this->logger->info('ProposalMatcher : completeMatchings '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        // we complete the matchings with the waypoints and criteria
        foreach ($matchings as $matching) {
            // waypoints
            foreach ($matching->getFilters()['route'] as $key => $point) {
                $waypoint = new Waypoint();
                $waypoint->setPosition($key);
                $waypoint->setDestination(false);
                if ($key == (count($matching->getFilters()['route']) - 1)) {
                    $waypoint->setDestination(true);
                }
                $waypoint->setAddress(clone $point['address']);
                $waypoint->setDuration($point['duration']);
                $waypoint->setRole($point['candidate']);
                $matching->addWaypoint($waypoint);
            }

            // criteria
            $matchingCriteria = new Criteria();
            $matchingCriteria->setDriver(true);
            // $direction = $matching->getFilters()['direction'];
            // $direction->setSaveGeoJson(false);
            // $matchingCriteria->setDirectionDriver($matching->getFilters()['direction']);
            $matchingCriteria->setFrequency(Criteria::FREQUENCY_PUNCTUAL);
            $matchingCriteria->setStrictDate($matching->getProposalOffer()->getCriteria()->isStrictDate());
            $matchingCriteria->setAnyRouteAsPassenger(true);

            // prices
            // we use the driver's priceKm
            $matchingCriteria->setPriceKm($matching->getProposalOffer()->getCriteria()->getPriceKm());

            // we use the passenger's computed prices
            // $matchingCriteria->setDriverComputedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedPrice());
            // $matchingCriteria->setDriverComputedRoundedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedRoundedPrice());
            // $matchingCriteria->setPassengerComputedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedPrice());
            // $matchingCriteria->setPassengerComputedRoundedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedRoundedPrice());

            // we use the driver's computed prices
            $matchingCriteria->setDriverComputedPrice(max(0, ($matching->getCommonDistance() + $matching->getDetourDistance()) * $matching->getProposalOffer()->getCriteria()->getPriceKm() / 1000));
            $matchingCriteria->setDriverComputedRoundedPrice($this->formatDataManager->roundPrice((float) $matchingCriteria->getDriverComputedPrice(), $matchingCriteria->getFrequency()));
            $matchingCriteria->setPassengerComputedPrice($matchingCriteria->getDriverComputedPrice());
            $matchingCriteria->setPassengerComputedRoundedPrice($matchingCriteria->getDriverComputedRoundedPrice());

            // frequency, fromDate and toDate
            if (Criteria::FREQUENCY_REGULAR == $matching->getProposalOffer()->getCriteria()->getFrequency() && Criteria::FREQUENCY_REGULAR == $matching->getProposalRequest()->getCriteria()->getFrequency()) {
                $matchingCriteria->setFrequency(Criteria::FREQUENCY_REGULAR);
                $matchingCriteria->setFromDate(max($matching->getProposalOffer()->getCriteria()->getFromDate(), $matching->getProposalRequest()->getCriteria()->getFromDate()));
                $matchingCriteria->setToDate(min($matching->getProposalOffer()->getCriteria()->getToDate(), $matching->getProposalRequest()->getCriteria()->getToDate()));
            } elseif (Criteria::FREQUENCY_PUNCTUAL == $matching->getProposalOffer()->getCriteria()->getFrequency()) {
                $matchingCriteria->setFromDate($matching->getProposalOffer()->getCriteria()->getFromDate());
                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFromTime());
            } else {
                $matchingCriteria->setFromDate($matching->getProposalRequest()->getCriteria()->getFromDate());
                $matchingCriteria->setFromTime($proposal->getCriteria()->getFromTime());
                if (is_null($proposal->getCriteria()->getFromTime()) || !$proposal->getUseTime()) {
                    // We don't need to adjust the time to include pickup duration
                    // We do it when the Ask is made so if we do it here, there will be a double offset
                    // $pickUpDropOffInfos = $this->getPickUpDropOffDurations($matching->getFilters()['route']);
                    // $pickUpDuration = round($pickUpDropOffInfos[0]);

                    // We need to find the first day after the current day carpooled by the driver and set the correct time because the request had no time
                    $currentDate = new \DateTime();
                    $currentDate->setTimestamp($matching->getProposalRequest()->getCriteria()->getFromDate()->getTimestamp());
                    $cptLoop = 0; // safeguard for infinity loop
                    $foundDay = false;
                    while ($cptLoop < 7 && false == $foundDay) {
                        switch ($currentDate->format('w')) {
                            case 0:
                                if ($matching->getProposalOffer()->getCriteria()->isSunCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSunTime());
                                    $foundDay = true;
                                }

                                break;

                            case 1:
                                if ($matching->getProposalOffer()->getCriteria()->isMonCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getMonTime());
                                    $foundDay = true;
                                }

                                break;

                            case 2:
                                if ($matching->getProposalOffer()->getCriteria()->isTueCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getTueTime());
                                    $foundDay = true;
                                }

                                break;

                            case 3:
                                if ($matching->getProposalOffer()->getCriteria()->isWedCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getWedTime());
                                    $foundDay = true;
                                }

                                break;

                            case 4:
                                if ($matching->getProposalOffer()->getCriteria()->isThuCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getThuTime());
                                    $foundDay = true;
                                }

                                break;

                            case 5:
                                if ($matching->getProposalOffer()->getCriteria()->isFriCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFriTime());
                                    $foundDay = true;
                                }

                                break;

                            case 6:
                                if ($matching->getProposalOffer()->getCriteria()->isSatCheck()) {
                                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSatTime());
                                    $foundDay = true;
                                }

                                break;
                        }
                        if ($foundDay) {
                            // We don't need to adjust the time to include pickup duration
                            // We do it when the Ask is made so if we do it here, there will be a double offset

                            // $timeAdjustedWithPickUp = clone $matchingCriteria->getFromTime();
                            // $timeAdjustedWithPickUp->modify('+'.$pickUpDuration.' seconds');
                            // $matchingCriteria->setFromTime($timeAdjustedWithPickUp);

                            // We set the real matching day. The first carpooled day by the driver
                            $matchingCriteria->setFromDate($currentDate);

                            break;
                        }
                        $currentDate->modify('+1 days');
                        ++$cptLoop;
                    }
                }
            }

            // seats (set to 1 for now)
            $matchingCriteria->setSeatsDriver(1);
            $matchingCriteria->setSeatsPassenger(1);

            // pickup times
            if (isset($matching->getFilters()['pickup']['minPickupTime'], $matching->getFilters()['pickup']['maxPickupTime'])) {
                if (Criteria::FREQUENCY_PUNCTUAL == $matching->getProposalOffer()->getCriteria()->getFrequency()) {
                    $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getMinTime());
                    $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getMaxTime());
                    $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getMarginDuration());
                    $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFromTime());
                } else {
                    switch ($matchingCriteria->getFromDate()->format('w')) {
                        case 0:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getSunMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getSunMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getSunMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSunTime());

                            break;

                        case 1:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getMonMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getMonMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getMonMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getMonTime());

                            break;

                        case 2:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getTueMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getTueMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getTueMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getTueTime());

                            break;

                        case 3:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getWedMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getWedMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getWedMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getWedTime());

                            break;

                        case 4:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getThuMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getThuMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getThuMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getThuTime());

                            break;

                        case 5:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getFriMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getFriMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getFriMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFriTime());

                            break;

                        case 6:
                            $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getSatMinTime());
                            $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getSatMaxTime());
                            $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getSatMarginDuration());
                            $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSatTime());

                            break;
                    }
                }
            }
            if (isset($matching->getFilters()['pickup']['monMinPickupTime'], $matching->getFilters()['pickup']['monMaxPickupTime'])) {
                $matchingCriteria->setMonCheck(true);
                $matchingCriteria->setMonMinTime($matching->getProposalOffer()->getCriteria()->getMonMinTime());
                $matchingCriteria->setMonMaxTime($matching->getProposalOffer()->getCriteria()->getMonMaxTime());
                $matchingCriteria->setMonMarginDuration($matching->getProposalOffer()->getCriteria()->getMonMarginDuration());
                $matchingCriteria->setMonTime($matching->getProposalOffer()->getCriteria()->getMonTime());
            }
            if (isset($matching->getFilters()['pickup']['tueMinPickupTime'], $matching->getFilters()['pickup']['tueMaxPickupTime'])) {
                $matchingCriteria->setTueCheck(true);
                $matchingCriteria->setTueMinTime($matching->getProposalOffer()->getCriteria()->getTueMinTime());
                $matchingCriteria->setTueMaxTime($matching->getProposalOffer()->getCriteria()->getTueMaxTime());
                $matchingCriteria->setTueMarginDuration($matching->getProposalOffer()->getCriteria()->getTueMarginDuration());
                $matchingCriteria->setTueTime($matching->getProposalOffer()->getCriteria()->getTueTime());
            }
            if (isset($matching->getFilters()['pickup']['wedMinPickupTime'], $matching->getFilters()['pickup']['wedMaxPickupTime'])) {
                $matchingCriteria->setWedCheck(true);
                $matchingCriteria->setWedMinTime($matching->getProposalOffer()->getCriteria()->getWedMinTime());
                $matchingCriteria->setWedMaxTime($matching->getProposalOffer()->getCriteria()->getWedMaxTime());
                $matchingCriteria->setWedMarginDuration($matching->getProposalOffer()->getCriteria()->getWedMarginDuration());
                $matchingCriteria->setWedTime($matching->getProposalOffer()->getCriteria()->getWedTime());
            }
            if (isset($matching->getFilters()['pickup']['thuMinPickupTime'], $matching->getFilters()['pickup']['thuMaxPickupTime'])) {
                $matchingCriteria->setThuCheck(true);
                $matchingCriteria->setThuMinTime($matching->getProposalOffer()->getCriteria()->getThuMinTime());
                $matchingCriteria->setThuMaxTime($matching->getProposalOffer()->getCriteria()->getThuMaxTime());
                $matchingCriteria->setThuMarginDuration($matching->getProposalOffer()->getCriteria()->getThuMarginDuration());
                $matchingCriteria->setThuTime($matching->getProposalOffer()->getCriteria()->getThuTime());
            }
            if (isset($matching->getFilters()['pickup']['friMinPickupTime'], $matching->getFilters()['pickup']['friMaxPickupTime'])) {
                $matchingCriteria->setFriCheck(true);
                $matchingCriteria->setFriMinTime($matching->getProposalOffer()->getCriteria()->getFriMinTime());
                $matchingCriteria->setFriMaxTime($matching->getProposalOffer()->getCriteria()->getFriMaxTime());
                $matchingCriteria->setFriMarginDuration($matching->getProposalOffer()->getCriteria()->getFriMarginDuration());
                $matchingCriteria->setFriTime($matching->getProposalOffer()->getCriteria()->getFriTime());
            }
            if (isset($matching->getFilters()['pickup']['satMinPickupTime'], $matching->getFilters()['pickup']['satMaxPickupTime'])) {
                $matchingCriteria->setSatCheck(true);
                $matchingCriteria->setSatMinTime($matching->getProposalOffer()->getCriteria()->getSatMinTime());
                $matchingCriteria->setSatMaxTime($matching->getProposalOffer()->getCriteria()->getSatMaxTime());
                $matchingCriteria->setSatMarginDuration($matching->getProposalOffer()->getCriteria()->getSatMarginDuration());
                $matchingCriteria->setSatTime($matching->getProposalOffer()->getCriteria()->getSatTime());
            }
            if (isset($matching->getFilters()['pickup']['sunMinPickupTime'], $matching->getFilters()['pickup']['sunMaxPickupTime'])) {
                $matchingCriteria->setSunCheck(true);
                $matchingCriteria->setSunMinTime($matching->getProposalOffer()->getCriteria()->getSunMinTime());
                $matchingCriteria->setSunMaxTime($matching->getProposalOffer()->getCriteria()->getSunMaxTime());
                $matchingCriteria->setSunMarginDuration($matching->getProposalOffer()->getCriteria()->getSunMarginDuration());
                $matchingCriteria->setSunTime($matching->getProposalOffer()->getCriteria()->getSunTime());
            }
            $matching->setCriteria($matchingCriteria);

            // we remove the direction from the filter to reduce the size of the returned object
            // (it is already affected to the driver direction)
            $filters = $matching->getFilters();
            unset($filters['direction']);
            $matching->setFilters($filters);

            // we complete the pickup and dropoff
            list($pickUp, $dropOff) = $this->getPickUpDropOffDurations($filters['route']);
            $matching->setPickUpDuration($pickUp);
            $matching->setDropOffDuration($dropOff);
        }
        // $this->logger->info('ProposalMatcher : end completeMatchings '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        return $matchings;
    }

    // DYNAMIC

    /**
     * Update Matching proposal entities for a proposal.
     *
     * @param Proposal $proposal            The proposal for which we want the matchings
     * @param bool     $excludeProposalUser Exclude the matching proposals made by the proposal user
     *
     * @return Proposal The proposal with the matchings
     */
    public function updateMatchingsForProposal(Proposal $proposal, bool $excludeProposalUser = true)
    {
        // $this->logger->info('ProposalMatcher : updateMatchingsForProposal #'.$proposal->getId().' '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        set_time_limit(360);

        // we search the matchings
        $matchings = $this->findMatchingProposals($proposal, $excludeProposalUser);

        // $this->logger->info('ProposalMatcher : matchings for #'.$proposal->getId().' : '.count($matchings).' '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

        // first, we will check if existing matchings are still valid
        // for matchings as request
        foreach ($proposal->getMatchingOffers() as $matchingOffer) {
            // here, $proposal == matchingOffer->getProposalRequest()
            $found = false;
            foreach ($matchings as $matching) {
                if ($matching->getProposalOffer()->getId() == $matchingOffer->getProposalOffer()->getId()) {
                    // this matching already exists => it is still valid
                    $found = true;
                    // update the matching
                    $this->updateMatchingWithMatching($matching, $matchingOffer);

                    break;
                }
            }
            if (!$found) {
                // the matching was not found => it is now invalid, we remove it unless there's a related ask
                if (!$this->checkRelatedAskForMatching($matchingOffer)) {
                    $proposal->removeMatchingOffer($matchingOffer);
                }
            }
        }
        // for matchings as offer
        foreach ($proposal->getMatchingRequests() as $matchingRequest) {
            // here, $proposal == matchingRequest->getProposalOffer()
            $found = false;
            foreach ($matchings as $matching) {
                if ($matching->getProposalRequest()->getId() == $matchingRequest->getProposalRequest()->getId()) {
                    // this matching already exists => it is still valid
                    $found = true;
                    // update the matching
                    $this->updateMatchingWithMatching($matching, $matchingRequest);

                    break;
                }
            }
            if (!$found) {
                // the matching was not found => it is now invalid, we remove it unless there's a related ask
                if (!$this->checkRelatedAskForMatching($matchingRequest)) {
                    $proposal->removeMatchingRequest($matchingRequest);
                }
            }
        }

        // second, we add the new matchings
        foreach ($matchings as $matching) {
            $found = false;
            if ($matching->getProposalOffer() === $proposal) {
                foreach ($proposal->getMatchingRequests() as $matchingRequest) {
                    if ($matchingRequest->getProposalRequest()->getId() == $matching->getProposalRequest()->getId()) {
                        $found = true;

                        break;
                    }
                }
                if (!$found) {
                    $proposal->addMatchingRequest($matching);
                }
            } else {
                foreach ($proposal->getMatchingOffers() as $matchingOffer) {
                    if ($matchingOffer->getProposalOffer()->getId() == $matching->getProposalOffer()->getId()) {
                        $found = true;

                        break;
                    }
                }
                if (!$found) {
                    $proposal->addMatchingOffer($matching);
                }
            }
        }

        return $proposal;
    }

    // MASS

    /**
     * Find potential matchings for multiple proposals at once.
     * These potential proposals must be validated using the geomatcher.
     */
    public function findPotentialMatchingsForProposals(array $proposalIds, bool $updateImport = true)
    {
        $this->print_mem(1);

        gc_enable();
        // we create chunks of proposals to avoid freezing
        $proposalsChunked = array_chunk($proposalIds, $this->params['importChunkSize'], true);

        $this->print_mem(2);

        foreach ($proposalsChunked as $proposalChunk) {
            $ids = [];
            foreach ($proposalChunk as $key => $proposalId) {
                $ids[] = $proposalId['id'];
            }

            if ($updateImport) {
                // update status to pending
                $q = $this->entityManager
                    ->createQuery('UPDATE App\Import\Entity\UserImport i set i.status = :status, i.treatmentJourneyStartDate=:treatmentDate WHERE i.id IN (SELECT ui.id FROM App\Import\Entity\UserImport ui JOIN ui.user u JOIN u.proposals p WHERE p.id IN ('.implode(',', $ids).'))')
                    ->setParameters([
                        'status' => UserImport::STATUS_MATCHING_PENDING,
                        'treatmentDate' => new \DateTime(),
                    ])
                ;
                $q->execute();
            }

            $this->print_mem(3);

            $proposals = $this->proposalRepository->findBy(['id' => $ids]);
            $potentialProposals = [];
            $this->logger->info('Start searching potentials | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
            foreach ($proposals as $proposal) {
                if ($proposal->getCriteria()->isDriver()) {
                    if ($proposalsFoundForProposal = $this->proposalRepository->findMatchingProposals($proposal, true, true)) {
                        $aproposals = [];
                        foreach ($proposalsFoundForProposal as $key => $proposalFound) {
                            if (!array_key_exists($proposalFound['pid'], $aproposals)) {
                                $aproposals[$proposalFound['pid']] = [
                                    'pid' => $proposalFound['pid'],
                                    'uid' => $proposalFound['uid'],
                                    'driver' => $proposalFound['driver'],
                                    'passenger' => $proposalFound['passenger'],
                                    'maxDetourDuration' => $proposalFound['maxDetourDuration'],
                                    'maxDetourDistance' => $proposalFound['maxDetourDistance'],
                                    'dpduration' => $proposalFound['dpduration'],
                                    'dpdistance' => $proposalFound['dpdistance'],
                                    'addresses' => [
                                        [
                                            'position' => $proposalFound['position'],
                                            'destination' => $proposalFound['destination'],
                                            'latitude' => $proposalFound['latitude'],
                                            'longitude' => $proposalFound['longitude'],
                                            'streetAddress' => $proposalFound['streetAddress'],
                                            'postalCode' => $proposalFound['postalCode'],
                                            'addressLocality' => $proposalFound['addressLocality'],
                                            'addressCountry' => $proposalFound['addressCountry'],
                                            'elevation' => $proposalFound['elevation'],
                                            'houseNumber' => $proposalFound['houseNumber'],
                                            'street' => $proposalFound['street'],
                                            'subLocality' => $proposalFound['subLocality'],
                                            'localAdmin' => $proposalFound['localAdmin'],
                                            'county' => $proposalFound['county'],
                                            'macroCounty' => $proposalFound['macroCounty'],
                                            'region' => $proposalFound['region'],
                                            'macroRegion' => $proposalFound['macroRegion'],
                                            'countryCode' => $proposalFound['countryCode'],
                                        ],
                                    ],
                                ];
                            } else {
                                $element = [
                                    'position' => $proposalFound['position'],
                                    'destination' => $proposalFound['destination'],
                                    'latitude' => $proposalFound['latitude'],
                                    'longitude' => $proposalFound['longitude'],
                                    'streetAddress' => $proposalFound['streetAddress'],
                                    'postalCode' => $proposalFound['postalCode'],
                                    'addressLocality' => $proposalFound['addressLocality'],
                                    'addressCountry' => $proposalFound['addressCountry'],
                                    'elevation' => $proposalFound['elevation'],
                                    'houseNumber' => $proposalFound['houseNumber'],
                                    'street' => $proposalFound['street'],
                                    'subLocality' => $proposalFound['subLocality'],
                                    'localAdmin' => $proposalFound['localAdmin'],
                                    'county' => $proposalFound['county'],
                                    'macroCounty' => $proposalFound['macroCounty'],
                                    'region' => $proposalFound['region'],
                                    'macroRegion' => $proposalFound['macroRegion'],
                                    'countryCode' => $proposalFound['countryCode'],
                                ];
                                if (!in_array($element, $aproposals[$proposalFound['pid']]['addresses'])) {
                                    $aproposals[$proposalFound['pid']]['addresses'][] = $element;
                                }
                            }
                            $proposalFound = null;
                            unset($proposalFound);
                        }
                        ksort($aproposals);

                        $potentialProposals[$proposal->getId()] = [
                            'proposal' => $proposal,
                            'potentials' => $aproposals,
                        ];
                    }
                    $proposalsFoundForProposal = null;
                    unset($proposalsFoundForProposal);
                }
            }
            foreach ($proposals as $proposal) {
                $proposal = null;
                unset($proposal);
            }
            $proposals = null;
            unset($proposals);
            gc_collect_cycles();
            $this->logger->info('End searching potentials | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

            $this->print_mem(4);

            // we create the candidates array
            $candidates = $this->createCandidates($potentialProposals);

            // clean
            foreach ($potentialProposals as $potential) {
                $potential = null;
                unset($potential);
            }
            $potentialProposals = null;
            unset($potentialProposals);
            gc_collect_cycles();

            $this->print_mem(5);

            // create the array for multimatch
            $this->logger->info('Start creating multimatch array | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
            $multimatch = [];
            foreach ($candidates as $item) {
                $multimatch[] = [
                    'driver' => $item['candidateProposal'],
                    'passengers' => $item['candidatesPassenger'],
                ];
            }
            $this->logger->info('End creating multimatch array, size : '.count($multimatch).' | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

            $this->print_mem(6);

            // create a batch for directions calculation
            $batches = array_chunk($multimatch, $this->params['importBatchMatchSize']);

            $potentialMatchings = []; // indexed by driver proposal id
            foreach ($batches as $key => $batch) {
                $this->logger->info('Start multimatch batch #'.$key.' | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                foreach ($batch as $key2 => $match) {
                    $this->logger->info('Match # '.$key2.', Passengers : '.count($match['passengers']).' | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                }
                if ($matches = $this->geoMatcher->multiMatch($batch)) {
                    foreach ($matches as $candidateDriverId => $candidatePassengers) {
                        foreach ($candidatePassengers as $candidatePassengerId => $cmatches) {
                            // we sort each possible matches as many matches can be found for 2 candidates : if multiple routes satisfy the criteria
                            switch (self::MULTI_MATCHES_FOR_SAME_CANDIDATES) {
                                case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_FASTEST:
                                    usort($cmatches, self::build_sorter('newDuration'));
                                    $matching = new Matching();
                                    $matching->setProposalOffer($this->proposalRepository->find($candidateDriverId));
                                    $matching->setProposalRequest($this->proposalRepository->find($candidatePassengerId));
                                    $matching->setFilters($cmatches[0]);
                                    $matching->setOriginalDistance($cmatches[0]['originalDistance']);
                                    $matching->setAcceptedDetourDistance($cmatches[0]['acceptedDetourDistance']);
                                    $matching->setNewDistance($cmatches[0]['newDistance']);
                                    $matching->setDetourDistance($cmatches[0]['detourDistance']);
                                    $matching->setDetourDistancePercent($cmatches[0]['detourDistancePercent']);
                                    $matching->setOriginalDuration($cmatches[0]['originalDuration']);
                                    $matching->setAcceptedDetourDuration($cmatches[0]['acceptedDetourDuration']);
                                    $matching->setNewDuration($cmatches[0]['newDuration']);
                                    $matching->setDetourDuration($cmatches[0]['detourDuration']);
                                    $matching->setDetourDurationPercent($cmatches[0]['detourDurationPercent']);
                                    $matching->setCommonDistance($cmatches[0]['commonDistance']);
                                    $potentialMatchings[$candidateDriverId][] = $matching;

                                    break;

                                case self::MULTI_MATCHES_FOR_SAME_CANDIDATES_SHORTEST:
                                    usort($cmatches, self::build_sorter('newDistance'));
                                    $matching = new Matching();
                                    $matching->setProposalOffer($this->proposalRepository->find($candidateDriverId));
                                    $matching->setProposalRequest($this->proposalRepository->find($candidatePassengerId));
                                    $matching->setFilters($cmatches[0]);
                                    $matching->setOriginalDistance($cmatches[0]['originalDistance']);
                                    $matching->setAcceptedDetourDistance($cmatches[0]['acceptedDetourDistance']);
                                    $matching->setNewDistance($cmatches[0]['newDistance']);
                                    $matching->setDetourDistance($cmatches[0]['detourDistance']);
                                    $matching->setDetourDistancePercent($cmatches[0]['detourDistancePercent']);
                                    $matching->setOriginalDuration($cmatches[0]['originalDuration']);
                                    $matching->setAcceptedDetourDuration($cmatches[0]['acceptedDetourDuration']);
                                    $matching->setNewDuration($cmatches[0]['newDuration']);
                                    $matching->setDetourDuration($cmatches[0]['detourDuration']);
                                    $matching->setDetourDurationPercent($cmatches[0]['detourDurationPercent']);
                                    $matching->setCommonDistance($cmatches[0]['commonDistance']);
                                    $potentialMatchings[$candidateDriverId][] = $matching;

                                    break;

                                default:
                                    break;
                            }
                        }
                        foreach ($cmatches as $match) {
                            $match = null;
                            unset($match);
                        }
                        $cmatches = null;
                        unset($cmatches);
                    }
                    $candidatePassengers = null;
                    unset($candidatePassengers);
                }
                $matches = null;
                unset($matches);
                gc_collect_cycles();
            }

            $this->print_mem(7);

            // clean
            foreach ($candidates as $item) {
                $item = null;
                unset($item);
            }
            foreach ($multimatch as $item) {
                $item = null;
                unset($item);
            }
            $multimatch = null;
            $candidates = null;
            $batch = null;
            $batches = null;
            unset($multimatch, $candidates, $batch, $batches);

            gc_collect_cycles();

            $this->print_mem(8);

            $matchings = [];
            foreach ($potentialMatchings as $proposalOfferId => $potentials) {
                // $proposal = $proposals[$proposalOfferId];
                $proposal = $this->proposalRepository->find($proposalOfferId);
                // if we use times, we check if the pickup times match
                if (
                    (Criteria::FREQUENCY_PUNCTUAL == $proposal->getCriteria()->getFrequency() && $proposal->getCriteria()->getFromTime())
                        || (Criteria::FREQUENCY_REGULAR == $proposal->getCriteria()->getFrequency() && (
                            ($proposal->getCriteria()->isMonCheck() && $proposal->getCriteria()->getMonTime())
                            || ($proposal->getCriteria()->isTueCheck() && $proposal->getCriteria()->getTueTime())
                            || ($proposal->getCriteria()->isWedCheck() && $proposal->getCriteria()->getWedTime())
                            || ($proposal->getCriteria()->isThuCheck() && $proposal->getCriteria()->getThuTime())
                            || ($proposal->getCriteria()->isFriCheck() && $proposal->getCriteria()->getFriTime())
                            || ($proposal->getCriteria()->isSatCheck() && $proposal->getCriteria()->getSatTime())
                            || ($proposal->getCriteria()->isSunCheck() && $proposal->getCriteria()->getSunTime())
                        ))
                ) {
                    $this->logger->info('Proposal matcher | Check pickup start | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                    $matchings = array_merge($matchings, $this->checkPickUp($potentials));
                    $this->logger->info('Proposal matcher | Check pickup end | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                }
                $potentials = null;
                unset($potentials);
                $proposal = null;
                unset($proposal);
                gc_collect_cycles();
            }
            $potentialMatchings = null;
            unset($potentialMatchings);
            gc_collect_cycles();

            $this->print_mem(9);

            // we complete the matchings with the waypoints and criteria
            $nb = 1;
            foreach ($matchings as $matching) {
                $this->logger->info('Proposal matcher | Complete matching '.$nb.' | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
                ++$nb;

                // waypoints
                foreach ($matching->getFilters()['route'] as $key => $point) {
                    $waypoint = new Waypoint();
                    $waypoint->setPosition($key);
                    $waypoint->setDestination(false);
                    if ($key == (count($matching->getFilters()['route']) - 1)) {
                        $waypoint->setDestination(true);
                    }
                    $waypoint->setAddress(clone $point['address']);
                    $waypoint->setDuration($point['duration']);
                    $waypoint->setRole($point['candidate']);
                    $matching->addWaypoint($waypoint);
                }

                // criteria
                $matchingCriteria = new Criteria();
                $matchingCriteria->setDriver(true);
                // $matchingCriteria->setDirectionDriver($matching->getFilters()['direction']);
                $matchingCriteria->setFrequency(Criteria::FREQUENCY_PUNCTUAL);
                $matchingCriteria->setStrictDate($matching->getProposalOffer()->getCriteria()->isStrictDate());
                $matchingCriteria->setAnyRouteAsPassenger(true);

                // prices
                // we use the driver's priceKm
                $matchingCriteria->setPriceKm($matching->getProposalOffer()->getCriteria()->getPriceKm());

                // we use the passenger's computed prices
                $matchingCriteria->setDriverComputedPrice(max(0, $matching->getProposalRequest()->getCriteria()->getPassengerComputedPrice()));
                $matchingCriteria->setDriverComputedRoundedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedRoundedPrice());
                $matchingCriteria->setPassengerComputedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedPrice());
                $matchingCriteria->setPassengerComputedRoundedPrice($matching->getProposalRequest()->getCriteria()->getPassengerComputedRoundedPrice());

                // frequency, fromDate and toDate
                if (Criteria::FREQUENCY_REGULAR == $matching->getProposalOffer()->getCriteria()->getFrequency() && Criteria::FREQUENCY_REGULAR == $matching->getProposalRequest()->getCriteria()->getFrequency()) {
                    $matchingCriteria->setFrequency(Criteria::FREQUENCY_REGULAR);
                    $matchingCriteria->setFromDate(max($matching->getProposalOffer()->getCriteria()->getFromDate(), $matching->getProposalRequest()->getCriteria()->getFromDate()));
                    $matchingCriteria->setToDate(min($matching->getProposalOffer()->getCriteria()->getToDate(), $matching->getProposalRequest()->getCriteria()->getToDate()));
                } elseif (Criteria::FREQUENCY_PUNCTUAL == $matching->getProposalOffer()->getCriteria()->getFrequency()) {
                    $matchingCriteria->setFromDate($matching->getProposalOffer()->getCriteria()->getFromDate());
                } else {
                    $matchingCriteria->setFromDate($matching->getProposalRequest()->getCriteria()->getFromDate());
                }

                // seats (set to 1 for now)
                $matchingCriteria->setSeatsDriver(1);
                $matchingCriteria->setSeatsPassenger(1);

                // pickup times
                if (isset($matching->getFilters()['pickup']['minPickupTime'], $matching->getFilters()['pickup']['maxPickupTime'])) {
                    if (Criteria::FREQUENCY_PUNCTUAL == $matching->getProposalOffer()->getCriteria()->getFrequency()) {
                        $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getMinTime());
                        $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getMaxTime());
                        $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getMarginDuration());
                        $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFromTime());
                    } else {
                        switch ($matchingCriteria->getFromDate()->format('w')) {
                            case 0:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getSunMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getSunMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getSunMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSunTime());

                                break;

                            case 1:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getMonMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getMonMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getMonMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getMonTime());

                                break;

                            case 2:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getTueMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getTueMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getTueMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getTueTime());

                                break;

                            case 3:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getWedMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getWedMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getWedMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getWedTime());

                                break;

                            case 4:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getThuMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getThuMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getThuMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getThuTime());

                                break;

                            case 5:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getFriMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getFriMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getFriMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getFriTime());

                                break;

                            case 6:
                                $matchingCriteria->setMinTime($matching->getProposalOffer()->getCriteria()->getSatMinTime());
                                $matchingCriteria->setMaxTime($matching->getProposalOffer()->getCriteria()->getSatMaxTime());
                                $matchingCriteria->setMarginDuration($matching->getProposalOffer()->getCriteria()->getSatMarginDuration());
                                $matchingCriteria->setFromTime($matching->getProposalOffer()->getCriteria()->getSatTime());

                                break;
                        }
                    }
                }
                if (isset($matching->getFilters()['pickup']['monMinPickupTime'], $matching->getFilters()['pickup']['monMaxPickupTime'])) {
                    $matchingCriteria->setMonCheck(true);
                    $matchingCriteria->setMonMinTime($matching->getProposalOffer()->getCriteria()->getMonMinTime());
                    $matchingCriteria->setMonMaxTime($matching->getProposalOffer()->getCriteria()->getMonMaxTime());
                    $matchingCriteria->setMonMarginDuration($matching->getProposalOffer()->getCriteria()->getMonMarginDuration());
                    $matchingCriteria->setMonTime($matching->getProposalOffer()->getCriteria()->getMonTime());
                }
                if (isset($matching->getFilters()['pickup']['tueMinPickupTime'], $matching->getFilters()['pickup']['tueMaxPickupTime'])) {
                    $matchingCriteria->setTueCheck(true);
                    $matchingCriteria->setTueMinTime($matching->getProposalOffer()->getCriteria()->getTueMinTime());
                    $matchingCriteria->setTueMaxTime($matching->getProposalOffer()->getCriteria()->getTueMaxTime());
                    $matchingCriteria->setTueMarginDuration($matching->getProposalOffer()->getCriteria()->getTueMarginDuration());
                    $matchingCriteria->setTueTime($matching->getProposalOffer()->getCriteria()->getTueTime());
                }
                if (isset($matching->getFilters()['pickup']['wedMinPickupTime'], $matching->getFilters()['pickup']['wedMaxPickupTime'])) {
                    $matchingCriteria->setWedCheck(true);
                    $matchingCriteria->setWedMinTime($matching->getProposalOffer()->getCriteria()->getWedMinTime());
                    $matchingCriteria->setWedMaxTime($matching->getProposalOffer()->getCriteria()->getWedMaxTime());
                    $matchingCriteria->setWedMarginDuration($matching->getProposalOffer()->getCriteria()->getWedMarginDuration());
                    $matchingCriteria->setWedTime($matching->getProposalOffer()->getCriteria()->getWedTime());
                }
                if (isset($matching->getFilters()['pickup']['thuMinPickupTime'], $matching->getFilters()['pickup']['thuMaxPickupTime'])) {
                    $matchingCriteria->setThuCheck(true);
                    $matchingCriteria->setThuMinTime($matching->getProposalOffer()->getCriteria()->getThuMinTime());
                    $matchingCriteria->setThuMaxTime($matching->getProposalOffer()->getCriteria()->getThuMaxTime());
                    $matchingCriteria->setThuMarginDuration($matching->getProposalOffer()->getCriteria()->getThuMarginDuration());
                    $matchingCriteria->setThuTime($matching->getProposalOffer()->getCriteria()->getThuTime());
                }
                if (isset($matching->getFilters()['pickup']['friMinPickupTime'], $matching->getFilters()['pickup']['friMaxPickupTime'])) {
                    $matchingCriteria->setFriCheck(true);
                    $matchingCriteria->setFriMinTime($matching->getProposalOffer()->getCriteria()->getFriMinTime());
                    $matchingCriteria->setFriMaxTime($matching->getProposalOffer()->getCriteria()->getFriMaxTime());
                    $matchingCriteria->setFriMarginDuration($matching->getProposalOffer()->getCriteria()->getFriMarginDuration());
                    $matchingCriteria->setFriTime($matching->getProposalOffer()->getCriteria()->getFriTime());
                }
                if (isset($matching->getFilters()['pickup']['satMinPickupTime'], $matching->getFilters()['pickup']['satMaxPickupTime'])) {
                    $matchingCriteria->setSatCheck(true);
                    $matchingCriteria->setSatMinTime($matching->getProposalOffer()->getCriteria()->getSatMinTime());
                    $matchingCriteria->setSatMaxTime($matching->getProposalOffer()->getCriteria()->getSatMaxTime());
                    $matchingCriteria->setSatMarginDuration($matching->getProposalOffer()->getCriteria()->getSatMarginDuration());
                    $matchingCriteria->setSatTime($matching->getProposalOffer()->getCriteria()->getSatTime());
                }
                if (isset($matching->getFilters()['pickup']['sunMinPickupTime'], $matching->getFilters()['pickup']['sunMaxPickupTime'])) {
                    $matchingCriteria->setSunCheck(true);
                    $matchingCriteria->setSunMinTime($matching->getProposalOffer()->getCriteria()->getSunMinTime());
                    $matchingCriteria->setSunMaxTime($matching->getProposalOffer()->getCriteria()->getSunMaxTime());
                    $matchingCriteria->setSunMarginDuration($matching->getProposalOffer()->getCriteria()->getSunMarginDuration());
                    $matchingCriteria->setSunTime($matching->getProposalOffer()->getCriteria()->getSunTime());
                }
                $matching->setCriteria($matchingCriteria);

                // we remove the direction from the filter to reduce the size of the returned object
                // (it is already affected to the driver direction)
                $filters = $matching->getFilters();
                // we complete the pickup and dropoff
                list($pickUp, $dropOff) = $this->getPickUpDropOffDurations($filters['route']);
                $matching->setPickUpDuration($pickUp);
                $matching->setDropOffDuration($dropOff);
                $filters['direction'] = null;
                unset($filters['direction']);
                $matching->setFilters($filters);
                $this->entityManager->persist($matching);
            }
            $this->logger->info('End multimatch | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

            $this->logger->info('Start flushing multimatch | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
            $this->entityManager->flush();
            $this->entityManager->clear();
            $this->logger->info('End flushing multimatch | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));

            $this->print_mem(10);

            // clean
            foreach ($matchings as $matching) {
                $matching = null;
                unset($matching);
            }
            $matchings = null;
            unset($matchings);
            // gc_collect_cycles();

            if ($updateImport) {
                // update status to treated
                $q = $this->entityManager
                    ->createQuery('UPDATE App\Import\Entity\UserImport i set i.status = :status, i.treatmentJourneyEndDate=:treatmentDate WHERE i.id IN (SELECT ui.id FROM App\Import\Entity\UserImport ui JOIN ui.user u JOIN u.proposals p WHERE p.id IN ('.implode(',', $ids).'))')
                    ->setParameters([
                        'status' => UserImport::STATUS_MATCHING_TREATED,
                        'treatmentDate' => new \DateTime(),
                    ])
                ;
                $q->execute();
            }

            $ids = null;
            unset($ids);
            $this->print_mem(11);
        }
    }

    // Params getters

    /**
     * Get the ALGORITHM_MAX_DETOUR_DISTANCE_PERCENT param's value.
     */
    public static function getMaxDetourDistancePercent(): int
    {
        return static::$maxDetourDistancePercent;
    }

    /**
     * Get the ALGORITHM_MAX_DETOUR_DURATION_PERCENT param's value.
     */
    public static function getMaxDetourDurationPercent(): int
    {
        return static::$maxDetourDurationPercent;
    }

    /**
     * Get the ALGORITHM_MIN_COMMON_DISTANCE_CHECK param's value.
     */
    public static function getMinCommonDistanceCheck(): int
    {
        return static::$minCommonDistanceCheck;
    }

    /**
     * Get the ALGORITHM_MIN_COMMON_DISTANCE_PERCENT param's value.
     */
    public static function getMinCommonDistancePercent(): int
    {
        return static::$minCommonDistancePercent;
    }

    /**
     * Callback function for array sort.
     *
     * @param mixed $key
     */
    private static function build_sorter($key)
    {
        return function ($a, $b) use ($key) {
            if ($a[$key] == $b[$key]) {
                return 0;
            }

            return ($a[$key] < $b[$key]) ? -1 : 1;
        };
    }

    /**
     * Check that pickup times are valid against the given proposals.
     *
     * @param array $matchings The candidates
     *
     * @return array
     */
    private function checkPickUp(array $matchings)
    {
        $validMatchings = [];
        foreach ($matchings as $matching) {
            $pickupDuration = null;
            $filters = $matching->getFilters();
            foreach ($filters['route'] as $value) {
                if (2 == $value['candidate'] && 0 == $value['position']) {
                    $pickupDuration = (int) round($value['duration']);

                    break;
                }
            }
            $validPickupTimes = $this->getValidPickupTimes($matching->getProposalOffer(), $matching->getProposalRequest(), $pickupDuration);
            if (count($validPickupTimes) > 0) {
                $filters['pickup'] = $validPickupTimes;
                $matching->setFilters($filters);
                $validMatchings[] = $matching;
            }
        }

        return $validMatchings;
    }

    /**
     * Get the valid pickup times for the given proposals
     * Valid = we check the times of both proposals to be sure that they match.
     *
     * @param Proposal $proposal1      The driver proposal
     * @param Proposal $proposal2      The passenger proposal
     * @param int      $pickupDuration The duration from the origin to the pickup point
     */
    private function getValidPickupTimes(Proposal $proposal1, Proposal $proposal2, int $pickupDuration): array
    {
        $pickupTime = $minPickupTime = $maxPickupTime = null;
        $monPickupTime = $monMinPickupTime = $monMaxPickupTime = null;
        $tuePickupTime = $tueMinPickupTime = $tueMaxPickupTime = null;
        $wedPickupTime = $wedMinPickupTime = $wedMaxPickupTime = null;
        $thuPickupTime = $thuMinPickupTime = $thuMaxPickupTime = null;
        $friPickupTime = $friMinPickupTime = $friMaxPickupTime = null;
        $satPickupTime = $satMinPickupTime = $satMaxPickupTime = null;
        $sunPickupTime = $sunMinPickupTime = $sunMaxPickupTime = null;

        switch ($proposal1->getCriteria()->getFrequency()) {
            case Criteria::FREQUENCY_PUNCTUAL:
                $pickupTime = clone $proposal1->getCriteria()->getFromTime();
                $minPickupTime = clone $proposal1->getCriteria()->getMinTime();
                $maxPickupTime = clone $proposal1->getCriteria()->getMaxTime();
                $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));

                switch ($proposal2->getCriteria()->getFrequency()) {
                    case Criteria::FREQUENCY_PUNCTUAL:
                        if (!(
                            ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                        )) {
                            // not in range
                            $pickupTime = null;
                            $minPickupTime = null;
                            $maxPickupTime = null;
                        }

                        break;

                    case Criteria::FREQUENCY_REGULAR:
                        switch ($proposal1->getCriteria()->getFromDate()->format('w')) {
                            case 0:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getSunMinTime() && $minPickupTime <= $proposal2->getCriteria()->getSunMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getSunMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getSunMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 1:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getMonMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMonMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getMonMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMonMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 2:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getTueMinTime() && $minPickupTime <= $proposal2->getCriteria()->getTueMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getTueMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getTueMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 3:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getWedMinTime() && $minPickupTime <= $proposal2->getCriteria()->getWedMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getWedMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getWedMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 4:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getThuMinTime() && $minPickupTime <= $proposal2->getCriteria()->getThuMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getThuMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getThuMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 5:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getFriMinTime() && $minPickupTime <= $proposal2->getCriteria()->getFriMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getFriMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getFriMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;

                            case 6:
                                if (!(
                                    ($minPickupTime >= $proposal2->getCriteria()->getSatMinTime() && $minPickupTime <= $proposal2->getCriteria()->getSatMaxTime())
                                    || ($maxPickupTime >= $proposal2->getCriteria()->getSatMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getSatMaxTime())
                                )) {
                                    // not in range
                                    $pickupTime = null;
                                    $minPickupTime = null;
                                    $maxPickupTime = null;
                                }

                                break;
                        }

                        break;
                }

                break;

            case Criteria::FREQUENCY_REGULAR:
                switch ($proposal2->getCriteria()->getFrequency()) {
                    case Criteria::FREQUENCY_PUNCTUAL:
                        if ($proposal1->getCriteria()->isMonCheck() && 1 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getMonTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getMonMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getMonMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isTueCheck() && 2 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getTueTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getTueMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getTueMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isWedCheck() && 3 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getWedTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getWedMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getWedMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isThuCheck() && 4 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getThuTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getThuMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getThuMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isFriCheck() && 5 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getFriTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getFriMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getFriMaxTime();
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isSatCheck() && 6 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getSatTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getSatMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getSatMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isSunCheck() && 0 == $proposal2->getCriteria()->getFromDate()->format('w')) {
                            $pickupTime = clone $proposal1->getCriteria()->getSunTime();
                            $minPickupTime = clone $proposal1->getCriteria()->getSunMinTime();
                            $maxPickupTime = clone $proposal1->getCriteria()->getSunMaxTime();
                            $pickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $minPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $maxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($minPickupTime >= $proposal2->getCriteria()->getMinTime() && $minPickupTime <= $proposal2->getCriteria()->getMaxTime())
                                || ($maxPickupTime >= $proposal2->getCriteria()->getMinTime() && $maxPickupTime <= $proposal2->getCriteria()->getMaxTime())
                            )) {
                                // not in range
                                $pickupTime = null;
                                $minPickupTime = null;
                                $maxPickupTime = null;
                            }
                        }

                        break;

                    case Criteria::FREQUENCY_REGULAR:
                        if ($proposal1->getCriteria()->isMonCheck() && $proposal2->getCriteria()->isMonCheck()) {
                            $monPickupTime = clone $proposal1->getCriteria()->getMonTime();
                            $monMinPickupTime = clone $proposal1->getCriteria()->getMonMinTime();
                            $monMaxPickupTime = clone $proposal1->getCriteria()->getMonMaxTime();
                            $monPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $monMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $monMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($monMinPickupTime >= $proposal2->getCriteria()->getMonMinTime() && $monMinPickupTime <= $proposal2->getCriteria()->getMonMaxTime())
                                || ($monMaxPickupTime >= $proposal2->getCriteria()->getMonMinTime() && $monMaxPickupTime <= $proposal2->getCriteria()->getMonMaxTime())
                            )) {
                                // not in range
                                $monPickupTime = null;
                                $monMinPickupTime = null;
                                $monMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isTueCheck() && $proposal2->getCriteria()->isTueCheck()) {
                            $tuePickupTime = clone $proposal1->getCriteria()->getTueTime();
                            $tueMinPickupTime = clone $proposal1->getCriteria()->getTueMinTime();
                            $tueMaxPickupTime = clone $proposal1->getCriteria()->getTueMaxTime();
                            $tuePickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $tueMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $tueMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($tueMinPickupTime >= $proposal2->getCriteria()->getTueMinTime() && $tueMinPickupTime <= $proposal2->getCriteria()->getTueMaxTime())
                                || ($tueMaxPickupTime >= $proposal2->getCriteria()->getTueMinTime() && $tueMaxPickupTime <= $proposal2->getCriteria()->getTueMaxTime())
                            )) {
                                // not in range
                                $tuePickupTime = null;
                                $tueMinPickupTime = null;
                                $tueMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isWedCheck() && $proposal2->getCriteria()->isWedCheck()) {
                            $wedPickupTime = clone $proposal1->getCriteria()->getWedTime();
                            $wedMinPickupTime = clone $proposal1->getCriteria()->getWedMinTime();
                            $wedMaxPickupTime = clone $proposal1->getCriteria()->getWedMaxTime();
                            $wedPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $wedMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $wedMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($wedMinPickupTime >= $proposal2->getCriteria()->getWedMinTime() && $wedMinPickupTime <= $proposal2->getCriteria()->getWedMaxTime())
                                || ($wedMaxPickupTime >= $proposal2->getCriteria()->getWedMinTime() && $wedMaxPickupTime <= $proposal2->getCriteria()->getWedMaxTime())
                            )) {
                                // not in range
                                $wedPickupTime = null;
                                $wedMinPickupTime = null;
                                $wedMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isThuCheck() && $proposal2->getCriteria()->isThuCheck()) {
                            $thuPickupTime = clone $proposal1->getCriteria()->getThuTime();
                            $thuMinPickupTime = clone $proposal1->getCriteria()->getThuMinTime();
                            $thuMaxPickupTime = clone $proposal1->getCriteria()->getThuMaxTime();
                            $thuPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $thuMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $thuMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($thuMinPickupTime >= $proposal2->getCriteria()->getThuMinTime() && $thuMinPickupTime <= $proposal2->getCriteria()->getThuMaxTime())
                                || ($thuMaxPickupTime >= $proposal2->getCriteria()->getThuMinTime() && $thuMaxPickupTime <= $proposal2->getCriteria()->getThuMaxTime())
                            )) {
                                // not in range
                                $thuPickupTime = null;
                                $thuMinPickupTime = null;
                                $thuMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isFriCheck() && $proposal2->getCriteria()->isFriCheck()) {
                            $friPickupTime = clone $proposal1->getCriteria()->getFriTime();
                            $friMinPickupTime = clone $proposal1->getCriteria()->getFriMinTime();
                            $friMaxPickupTime = clone $proposal1->getCriteria()->getFriMaxTime();
                            $friPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $friMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $friMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($friMinPickupTime >= $proposal2->getCriteria()->getFriMinTime() && $friMinPickupTime <= $proposal2->getCriteria()->getFriMaxTime())
                                || ($friMaxPickupTime >= $proposal2->getCriteria()->getFriMinTime() && $friMaxPickupTime <= $proposal2->getCriteria()->getFriMaxTime())
                            )) {
                                // not in range
                                $friPickupTime = null;
                                $friMinPickupTime = null;
                                $friMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isSatCheck() && $proposal2->getCriteria()->isSatCheck()) {
                            $satPickupTime = clone $proposal1->getCriteria()->getSatTime();
                            $satMinPickupTime = clone $proposal1->getCriteria()->getSatMinTime();
                            $satMaxPickupTime = clone $proposal1->getCriteria()->getSatMaxTime();
                            $satPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $satMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $satMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($satMinPickupTime >= $proposal2->getCriteria()->getSatMinTime() && $satMinPickupTime <= $proposal2->getCriteria()->getSatMaxTime())
                                || ($satMaxPickupTime >= $proposal2->getCriteria()->getSatMinTime() && $satMaxPickupTime <= $proposal2->getCriteria()->getSatMaxTime())
                            )) {
                                // not in range
                                $satPickupTime = null;
                                $satMinPickupTime = null;
                                $satMaxPickupTime = null;
                            }
                        }
                        if ($proposal1->getCriteria()->isSunCheck() && $proposal2->getCriteria()->isSunCheck()) {
                            $sunPickupTime = clone $proposal1->getCriteria()->getSunTime();
                            $sunMinPickupTime = clone $proposal1->getCriteria()->getSunMinTime();
                            $sunMaxPickupTime = clone $proposal1->getCriteria()->getSunMaxTime();
                            $sunPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $sunMinPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            $sunMaxPickupTime->add(new \DateInterval('PT'.$pickupDuration.'S'));
                            if (!(
                                ($sunMinPickupTime >= $proposal2->getCriteria()->getSunMinTime() && $sunMinPickupTime <= $proposal2->getCriteria()->getSunMaxTime())
                                || ($sunMaxPickupTime >= $proposal2->getCriteria()->getSunMinTime() && $sunMaxPickupTime <= $proposal2->getCriteria()->getSunMaxTime())
                            )) {
                                // not in range
                                $sunPickupTime = null;
                                $sunMinPickupTime = null;
                                $sunMaxPickupTime = null;
                            }
                        }

                        break;
                }

                break;
        }
        $return = [];
        if ($pickupTime) {
            $return['pickupTime'] = $pickupTime;
        }
        if ($minPickupTime) {
            $return['minPickupTime'] = $minPickupTime;
        }
        if ($maxPickupTime) {
            $return['maxPickupTime'] = $maxPickupTime;
        }
        if ($monPickupTime) {
            $return['monPickupTime'] = $monPickupTime;
        }
        if ($monMinPickupTime) {
            $return['monMinPickupTime'] = $monMinPickupTime;
        }
        if ($monMaxPickupTime) {
            $return['monMaxPickupTime'] = $monMaxPickupTime;
        }
        if ($tuePickupTime) {
            $return['tuePickupTime'] = $tuePickupTime;
        }
        if ($tueMinPickupTime) {
            $return['tueMinPickupTime'] = $tueMinPickupTime;
        }
        if ($tueMaxPickupTime) {
            $return['tueMaxPickupTime'] = $tueMaxPickupTime;
        }
        if ($wedPickupTime) {
            $return['wedPickupTime'] = $wedPickupTime;
        }
        if ($wedMinPickupTime) {
            $return['wedMinPickupTime'] = $wedMinPickupTime;
        }
        if ($wedMaxPickupTime) {
            $return['wedMaxPickupTime'] = $wedMaxPickupTime;
        }
        if ($thuPickupTime) {
            $return['thuPickupTime'] = $thuPickupTime;
        }
        if ($thuMinPickupTime) {
            $return['thuMinPickupTime'] = $thuMinPickupTime;
        }
        if ($thuMaxPickupTime) {
            $return['thuMaxPickupTime'] = $thuMaxPickupTime;
        }
        if ($friPickupTime) {
            $return['friPickupTime'] = $friPickupTime;
        }
        if ($friMinPickupTime) {
            $return['friMinPickupTime'] = $friMinPickupTime;
        }
        if ($friMaxPickupTime) {
            $return['friMaxPickupTime'] = $friMaxPickupTime;
        }
        if ($satPickupTime) {
            $return['satPickupTime'] = $satPickupTime;
        }
        if ($satMinPickupTime) {
            $return['satMinPickupTime'] = $satMinPickupTime;
        }
        if ($satMaxPickupTime) {
            $return['satMaxPickupTime'] = $satMaxPickupTime;
        }
        if ($sunPickupTime) {
            $return['sunPickupTime'] = $sunPickupTime;
        }
        if ($sunMinPickupTime) {
            $return['sunMinPickupTime'] = $sunMinPickupTime;
        }
        if ($sunMaxPickupTime) {
            $return['sunMaxPickupTime'] = $sunMaxPickupTime;
        }

        return $return;
    }

    /**
     * Return the pick up and drop off of a passenger in a route.
     *
     * @return array The pick up and drop off
     */
    private function getPickUpDropOffDurations(array $route)
    {
        $pickUp = 0;
        $dropOff = 0;
        $position = 0;
        foreach ($route as $point) {
            if (1 == $point['candidate']) {
                continue;
            }
            if (0 == $point['position']) {
                $pickUp = $point['duration'];
            } elseif ($point['position'] > $position) {
                $position = $point['position'];
                $dropOff = $point['duration'];
            }
        }

        return [$pickUp, $dropOff];
    }

    /**
     * Check if there's a related ask to a matching. This method could be useless (we just need to count getAsks !) but here we also update the status if needed.
     *
     * @param Matching $matching The matching
     *
     * @return bool The result
     */
    private function checkRelatedAskForMatching(Matching $matching)
    {
        foreach ($matching->getAsks() as $ask) {
            /**
             * @var Ask $ask
             */
            // if the passenger has made an ask
            if (Ask::STATUS_PENDING_AS_PASSENGER == $ask->getStatus()) {
                // check the validity of the ask => must be less than DYNAMIC_CARPOOL_MAX_PENDING_TIME seconds
                $limit = clone $ask->getCreatedDate();
                $limit->add(new \DateInterval('PT'.$this->params['dynamicMaxPendingTime'].'S'));
                $now = new \DateTime('UTC');
                if ($limit < $now) {
                    $ask->setStatus(Ask::STATUS_DECLINED_AS_DRIVER);
                    $this->entityManager->persist($ask);
                    $this->entityManager->flush();
                }
            }
        }

        return count($matching->getAsks()) > 0;
    }

    /**
     * Copy informations between a matching to another.
     * Useful for updating a matching with new data.
     *
     * @param Matching $sourceMatching      The source matching
     * @param Matching $destinationMatching The destination matching
     *
     * @return Matching The updated matching
     */
    private function updateMatchingWithMatching(Matching $sourceMatching, Matching $destinationMatching)
    {
        // matching properties
        $destinationMatching->setOriginalDistance($sourceMatching->getOriginalDistance());
        $destinationMatching->setAcceptedDetourDistance($sourceMatching->getAcceptedDetourDistance());
        $destinationMatching->setNewDistance($sourceMatching->getNewDistance());
        $destinationMatching->setDetourDistance($sourceMatching->getDetourDistance());
        $destinationMatching->setDetourDistancePercent($sourceMatching->getDetourDistancePercent());
        $destinationMatching->setOriginalDuration($sourceMatching->getOriginalDuration());
        $destinationMatching->setAcceptedDetourDuration($sourceMatching->getAcceptedDetourDuration());
        $destinationMatching->setNewDuration($sourceMatching->getNewDuration());
        $destinationMatching->setDetourDuration($sourceMatching->getDetourDuration());
        $destinationMatching->setDetourDurationPercent($sourceMatching->getDetourDurationPercent());
        $destinationMatching->setCommonDistance($sourceMatching->getCommonDistance());
        $destinationMatching->setPickUpDuration($sourceMatching->getPickUpDuration());
        $destinationMatching->setDropOffDuration($sourceMatching->getDropOffDuration());

        // matching waypoints => we replace old waypoints with the new ones
        foreach ($destinationMatching->getWaypoints() as $waypoint) {
            $destinationMatching->removeWaypoint($waypoint);
        }
        foreach ($sourceMatching->getWaypoints() as $waypoint) {
            $destinationMatching->addWaypoint($waypoint);
        }

        $this->entityManager->persist($destinationMatching);
        $this->entityManager->flush();
    }

    /**
     * Create candidates for potential proposals.
     */
    private function createCandidates(array $potentialProposals)
    {
        $candidates = [];
        $this->logger->info('Start creating candidates | '.(new \DateTime('UTC'))->format('Ymd H:i:s.u'));
        foreach ($potentialProposals as $proposalId => $potentialArray) {
            $proposal = $potentialArray['proposal'];
            $proposalsFound = $potentialArray['potentials'];
            $candidateProposal = new Candidate();
            $candidateProposal->setId($proposal->getId());
            $addresses = [];
            foreach ($proposal->getWaypoints() as $waypoint) {
                $addresses[] = $waypoint->getAddress();
            }
            $candidateProposal->setAddresses($addresses);
            $candidatesPassenger = [];

            $candidateProposal->setMaxDetourDistance($proposal->getCriteria()->getMaxDetourDistance() ? $proposal->getCriteria()->getMaxDetourDistance() : ($proposal->getCriteria()->getDirectionDriver()->getDistance() * static::$maxDetourDistancePercent / 100));
            $candidateProposal->setMaxDetourDuration($proposal->getCriteria()->getMaxDetourDuration() ? $proposal->getCriteria()->getMaxDetourDuration() : ($proposal->getCriteria()->getDirectionDriver()->getDuration() * static::$maxDetourDurationPercent / 100));
            $candidateProposal->setDirection($proposal->getCriteria()->getDirectionDriver());
            foreach ($proposalsFound as $proposalToMatch) {
                // if the candidate is not passenger we skip (the 2 candidates could be driver AND passenger, and the second one match only as a driver)
                if (!$proposalToMatch['passenger']) {
                    continue;
                }
                $candidate = new Candidate();
                $candidate->setId($proposalToMatch['pid']);
                $addressesCandidate = [];
                usort($proposalToMatch['addresses'], function ($a, $b) {
                    return $a['position'] <=> $b['position'];
                });
                foreach ($proposalToMatch['addresses'] as $waypoint) {
                    $address = new Address();
                    $address->setLatitude($waypoint['latitude']);
                    $address->setLongitude($waypoint['longitude']);
                    $address->setHouseNumber($waypoint['houseNumber']);
                    $address->setStreet($waypoint['street']);
                    $address->setStreetAddress($waypoint['streetAddress']);
                    $address->setPostalCode($waypoint['postalCode']);
                    $address->setSubLocality($waypoint['subLocality']);
                    $address->setAddressLocality($waypoint['addressLocality']);
                    $address->setLocalAdmin($waypoint['localAdmin']);
                    $address->setCounty($waypoint['county']);
                    $address->setMacroCounty($waypoint['macroCounty']);
                    $address->setRegion($waypoint['region']);
                    $address->setMacroRegion($waypoint['macroRegion']);
                    $address->setAddressCountry($waypoint['addressCountry']);
                    $address->setCountryCode($waypoint['countryCode']);
                    $address->setElevation($waypoint['elevation']);
                    $addressesCandidate[] = $address;
                }
                $candidate->setAddresses($addressesCandidate);
                $candidate->setDuration($proposalToMatch['dpduration']);
                $candidate->setDistance($proposalToMatch['dpdistance']);

                // the 2 following are not taken in account right now as only the driver detour matters
                $candidate->setMaxDetourDistance($proposalToMatch['maxDetourDistance'] ? $proposalToMatch['maxDetourDistance'] : ($proposalToMatch['dpdistance'] * static::$maxDetourDistancePercent / 100));
                $candidate->setMaxDetourDuration($proposalToMatch['maxDetourDuration'] ? $proposalToMatch['maxDetourDuration'] : ($proposalToMatch['dpduration'] * static::$maxDetourDurationPercent / 100));
                $candidatesPassenger[] = $candidate;
            }

            $candidates[$proposalId] = [
                'proposal' => $proposal,
                'candidateProposal' => $candidateProposal,
                'candidatesPassenger' => $candidatesPassenger,
            ];
        }

        return $candidates;
    }

    private function print_mem($id)
    {
        // Currently used memory
        $mem_usage = memory_get_usage();

        // Peak memory usage
        $mem_peak = memory_get_peak_usage();
        $this->logger->debug($id.' The script is now using: '.round($mem_usage / 1024).'KB of memory.<br>');
        $this->logger->debug($id.' Peak usage: '.round($mem_peak / 1024).'KB of memory.<br><br>');
    }
}