<?php

namespace AppBundle\Command;

use AppBundle\Entity\OperationPEE;
use AppBundle\Enum\OperationPEEState;
use Doctrine\ORM\EntityManagerInterface;
use Geocoder\Collection;
use Geocoder\Model\Coordinates;
use Geocoder\Query\GeocodeQuery;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Stopwatch\Stopwatch;
use Symfony\Component\Validator\Validator\ValidatorInterface;

/**
 * Geocode the operation's personal location, using BazingaGeocoderBundle.
 * Call : php7.2 bin/console cgenial:operationpee:geocode google_maps 0
 * 
 * @author Dimitri KURC <technique@agencehpj.fr>
 */
class GeocodeOperationPEELocationCommand extends ContainerAwareCommand
{
    /**
     * 
     */
    protected function configure()
    {
        $this
            ->setName('cgenial:operationpee:geocode')
            ->setDescription("
                Update the operation PEE coordinates, according to their location, using Google Maps.
                Only operations who do not have their coordinates not set will be updated. You can force this behavior.
                Example : php bin/console cgenial:operationpee:geocode google_maps 0")
            ->addArgument('provider', InputArgument::REQUIRED, 'BazingaGeocoderBundle provider name.')
            ->addArgument('max', InputArgument::REQUIRED, 'Maximum number of operations that you want to geocode. Set it to 0 if you want to include every operations.')
            ->addOption('all', null, InputOption::VALUE_NONE, 'Disregarding states')
            ->addOption('force', null, InputOption::VALUE_NONE, 'Force the geocoding of operations who already have their coordinates set.')
        ;
    }

    /**
     *
     * @param InputInterface $input
     * @param OutputInterface $output
     * @throws \Doctrine\ORM\ORMException
     * @throws \Doctrine\ORM\OptimisticLockException
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $container = $this->getContainer();

        $em = $container->get('doctrine.orm.entity_manager');
        $geocoder = $container->get('bazinga_geocoder.provider.' . $input->getArgument('provider'));
        $qtyUpdated = 0;
        $stopwatch = new Stopwatch();
        $validator = $container->get('validator');

        $operations = $this->findOperations($em, $validator, $input->getArgument('max'), $input->getOption('all'), $input->getOption('force'));
        
        $progress = new ProgressBar($output, count($operations));

        // Prevent Google Maps request usage limits for free accounts (50 every 1 second)
        // @see https://developers.google.com/maps/documentation/geocoding/usage-limits
        // This is purely theorical, because it assumes that the code takes 0 seconds to execute.
        $chunks = array_chunk($operations, 50);

        $progress->start();

        foreach ($chunks as $chunk) {
            $stopwatch->start('chunk');

            foreach ($chunk as $operation) {
                $geocodeQuery = $this->createGeocodeQuery($operation);
        
                $adresses = $geocoder->geocodeQuery($geocodeQuery);
                
                $isOperationUpdated = $this->updateOperationCoordinates($operation, $adresses);

                if ($isOperationUpdated) {
                    $qtyUpdated = $qtyUpdated + 1;
                    $em->persist($operation);
                }

                $progress->advance();
            }
            
            $event = $stopwatch->stop('chunk');

            if ($duration = $event->getDuration() < 1000) {
                usleep((1000 - $duration) * 1000);
            }
        }

        $progress->finish();
        $output->writeln('');

        if ($qtyUpdated > 0) {
            $output->writeln('Saving to database...');
            $em->flush();
        }

        $output->writeln(count($operations) . ' operation(s) geocoded. ' . $qtyUpdated . ' operation(s) updated.');
    }
    
    /**
     * Find geocodable operations from the DB, where the personalAddress and personalZipcode properties
     * has been validated using the Symfony validator.
     * The property validation is needed because the validation was absent
     * from the beginning. It means that some operation accounts could have bad formatted data.
     * 
     * @param EntityManagerInterface $em
     * @param ValidatorInterface $validator
     * @param int $max
     * @param bool $force
     * @return array List of operations
     */
    private function findOperations(
        EntityManagerInterface $em,
        ValidatorInterface $validator,
        int $max = 0,
        bool $all_states = false,
        bool $force = false
    ): array {
        $qb = $em->createQueryBuilder();

        // Find operations from the database where the personal address or zipcode is set
        $qb->select('o')
            ->from(OperationPEE::class, 'o')
            ->where(
                $qb->expr()->orX(
                    $qb->expr()->isNotNull('o.address'),
                    $qb->expr()->isNotNull('o.zipCode')
                )
            );

        if (!$all_states) {
            $qb->andWhere('o.state IN (:oState)')
                ->setParameter('oState', array(OperationPEEState::OPEN, OperationPEEState::FENCED));
        }

        if (!$force) {
            $qb->andWhere($qb->expr()->isNull('o.latitude'))
                ->andWhere($qb->expr()->isNull('o.longitude'));
        }

        if ($max > 0) {
            $qb->setMaxResults($max);
        }

        $operations = $qb->getQuery()->getResult();
        
        // Keep the operations were the Symfony validation is ok for the 2 fields
        // return array_filter($operations, function (OperationPEE $operation) use ($validator) {
        //     $addressViolations = $validator->validateProperty($operation, 'address', 'profile');
        //     $zipcodeViolations = $validator->validateProperty($operation, 'zipcode', 'profile');

        //     return 0 === count($addressViolations) && 0 === count($zipcodeViolations);
        // });
        
        return $operations;
    }

    /**
     * Format the query for the geocoding service
     *
     * @param OperationPEE $operation
     * @return GeocodeQuery 
     */
    private function createGeocodeQuery(OperationPEE $operation): GeocodeQuery
    {
        $address = $operation->getAddress();

        $location = ($address ? $address . ', ' : '') . $operation->getZipcode();

        return GeocodeQuery::create($location)->withLocale('fr');
    }

    /**
     * Update the operation personal coordinates, from a list of geocoded adresses
     *
     * @param OperationPEE $operation
     * @param Collection $adresses List of geocoded adresses
     * @return bool Indicates if the operation has been updated
     */
    private function updateOperationCoordinates(OperationPEE &$operation, Collection $adresses): bool
    {
        $isUpdated = false;

        if (!$adresses->isEmpty()) {
            // Assume the first address is the one we need
            $coordinates = $adresses->first()->getCoordinates();
            
            // Update the operation with the latitude and longitude found
            if ($coordinates instanceof Coordinates) {
                $operation->setLatitude($coordinates->getLatitude());
                $operation->setLongitude($coordinates->getLongitude());

                $isUpdated = true;
            }
        }

        return $isUpdated;
    }
}
