<?php
/**
 * This file is part of the DATAtourisme project.
 *
 *  @author Conjecto <contact@conjecto.com>
 *
 * For the full copyright and license information, please view the LICENSE file that was distributed with this source code.
 */

namespace Datatourisme\Api\Resolver\SparqlResolver;

use Datatourisme\Api\Exception\RuntimeException;
use Datatourisme\Api\Exception\TimeoutException;
use Datatourisme\Api\Resolver\ResolverInterface;
use Datatourisme\Api\Resolver\SparqlResolver\QueryBuilder\FilterGenerator;
use Datatourisme\Api\Resolver\SparqlResolver\QueryBuilder\QueryBuilderFactory;
use Datatourisme\Api\Resolver\SparqlResolver\QueryBuilder\QueryGenerator;
use Datatourisme\Api\Resolver\SparqlResolver\QueryBuilder\SortGenerator;
use Datatourisme\Api\Resolver\SparqlResolver\Utils\SparqlUtils;
use Datatourisme\Api\Schema\AbstractSchema;
use Youshido\GraphQL\Execution\ResolveInfo;
use Youshido\GraphQL\Field\InputFieldInterface;

class SparqlResolver implements ResolverInterface
{
    /**
     * @var string
     */
    protected $endpoint;

    /**
     * @var int
     */
    protected $timeout;

    /**
     * @var QueryBuilderFactory
     */
    protected $queryBuilderFactory;

    /**
     * @var QueryGenerator
     */
    protected $queryGenerator;

    /**
     * @var SortGenerator
     */
    protected $sortGenerator;

    /**
     * @var FilterGenerator
     */
    protected $filterGenerator;

    /**
     * @var QueryHydrator
     */
    protected $queryHydrator;

    /**
     * SparqlResolver constructor.
     *
     * @param string $endpoint
     */
    public function __construct($endpoint)
    {
        $this->endpoint = $endpoint;
        $this->timeout = 0;
    }

    /**
     * @return string
     */
    public function getEndpoint()
    {
        return $this->endpoint;
    }

    /**
     * @param string $endpoint
     *
     * @return $this
     */
    public function setEndpoint($endpoint)
    {
        $this->endpoint = $endpoint;

        return $this;
    }

    /**
     * @return int
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * Set query timeout, in milliseconds
     * 
     * @param string $timeout
     *
     * @return $this
     */
    public function setTimeout($timeout)
    {
        $this->timeout = $timeout;

        return $this;
    }

    /**
     * @return QueryGenerator
     */
    public function getQueryGenerator()
    {
        if (null == $this->queryGenerator) {
            $this->queryGenerator = new QueryGenerator($this->getQueryBuilderFactory(), $this->getFilterGenerator(), $this->getSortGenerator());
        }

        return $this->queryGenerator;
    }

    /**
     * @param QueryGenerator $queryGenerator
     */
    public function setQueryGenerator($queryGenerator)
    {
        $this->queryGenerator = $queryGenerator;
    }

    /**
     * @return SortGenerator
     */
    public function getSortGenerator()
    {
        if (null == $this->sortGenerator) {
            $this->sortGenerator = new SortGenerator();
        }

        return $this->sortGenerator;
    }

    /**
     * @param QueryBuilderFactory $queryBuilderFactory
     */
    public function setQueryBuilderFactory($queryBuilderFactory)
    {
        $this->queryBuilderFactory = $queryBuilderFactory;
    }

    /**
     * @return QueryBuilderFactory
     */
    public function getQueryBuilderFactory()
    {
        if (null == $this->queryBuilderFactory) {
            $this->queryBuilderFactory = new QueryBuilderFactory();
        }

        return $this->queryBuilderFactory;
    }

    /**
     * @param SortGenerator $sortGenerator
     */
    public function setSortGenerator($sortGenerator)
    {
        $this->sortGenerator = $sortGenerator;
    }

    /**
     * @return QueryHydrator
     */
    public function getQueryHydrator()
    {
        if (null == $this->queryHydrator) {
            $this->queryHydrator = new QueryHydrator();
        }

        return $this->queryHydrator;
    }

    /**
     * @param QueryHydrator $queryHydrator
     */
    public function setQueryHydrator($queryHydrator)
    {
        $this->queryHydrator = $queryHydrator;
    }

    /**
     * @return FilterGenerator
     */
    public function getFilterGenerator()
    {
        if (null == $this->filterGenerator) {
            $this->filterGenerator = new FilterGenerator();
        }

        return $this->filterGenerator;
    }

    /**
     * @param FilterGenerator $filterGenerator
     */
    public function setFilterGenerator($filterGenerator)
    {
        $this->filterGenerator = $filterGenerator;
    }

    /**
     * Resolve an execution context.
     *
     * @param string $entryPoint
     * @param $arguments
     * @param ResolveInfo $info
     *
     * @return array
     *
     * @throws \Exception
     */
    public function resolve(string $entryPoint, $arguments, ResolveInfo $info)
    {
        /** @var AbstractSchema $schema */
        $schema = $info->getExecutionContext()->getSchema();
        SparqlUtils::$prefixMap = $schema->getPrefixMap();

        $query = null;
        $total = null;
        $results = [];
        $stats = [];

        /** @var InputFieldInterface $inputFieldDef */
        $inputFieldDef = $info->getField()->getArguments()['filters'];
        $outputTypeDef = $info->getField()->getType()->getField('results')->getType()->getItemType();

        $queryAst = $info->getFieldAST('results');
        $astFields = $queryAst ? $queryAst->getFields() : [];

        $query = $this->getQueryGenerator()->getQuery($arguments, $astFields, $inputFieldDef, $outputTypeDef);

        if ($queryAst) {
            $mtime = microtime(true);
            $sparqlResults = $this->execute((string) $query);
            $stats['execution'] = (microtime(true) - $mtime) * 1000;

            $mtime = microtime(true);
            $results = $this->getQueryHydrator()->generate($arguments, $astFields, $sparqlResults);
            $stats['hydration'] = (microtime(true) - $mtime) * 1000;
        }

        if (null != $info->getFieldAST('total')) {
            $countQuery = (string) $this->getQueryGenerator()->getCountQuery($arguments, $inputFieldDef);

            $mtime = microtime(true);
            $total = $this->execute($countQuery, true);
            $stats['count'] = (microtime(true) - $mtime) * 1000;
        }

        $stats['total'] = array_sum($stats);

        return array(
            'total' => $total,
            'results' => $results,
            'query' => $query,
            'stats' => $stats,
        );
    }

    public function test($callback)
    {
        $time_start = microtime(true);
        $callback();
        $time_end = microtime(true);

        return ($time_end - $time_start) * 1000;
    }

    /**
     * @param $sparql
     * @param bool $count
     *
     * @return mixed
     *
     * @throws \Exception
     */
    public function execute($sparql, $count = false)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->endpoint);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, 'query='.urlencode($sparql));
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept:text/plain'));
        if ($this->getTimeout() > 0) {
            curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-BIGDATA-MAX-QUERY-MILLIS:' . $this->getTimeout()));
        }

        $result = curl_exec($ch);
        if (false === $result) {
            $code = curl_errno($ch);
            switch ($code) {
                case CURLE_COULDNT_CONNECT:
                    throw new RuntimeException('sparql endpoint unreachable');
            }
            throw new \Exception(curl_error($ch));
        }
        curl_close($ch);

        // timeout ?
        if (preg_match('/java\.util\.concurrent\.TimeoutException/i', $result)) {
            throw new TimeoutException("Query timed out");
        }

        if ($count) {
            preg_match('/<literal[^>]+>(\d+)/i', $result, $matches);
            if (isset($matches[1])) {
                return intval($matches[1]);
            }

            return 0;
        }

        return $result;
    }

    /**
     * @param $latitudeProperty
     *
     * @return $this
     */
    public function setLatitudeProperty($latitudeProperty)
    {
        $this->getFilterGenerator()->getFilter('_geo_bounding')->setLatitudeProperty($latitudeProperty);
        $this->getFilterGenerator()->getFilter('_geo_distance')->setLatitudeProperty($latitudeProperty);

        return $this;
    }

    /**
     * @param $longitudeProperty
     *
     * @return $this
     */
    public function setLongitudeProperty($longitudeProperty)
    {
        $this->getFilterGenerator()->getFilter('_geo_bounding')->setLongitudeProperty($longitudeProperty);
        $this->getFilterGenerator()->getFilter('_geo_distance')->setLongitudeProperty($longitudeProperty);

        return $this;
    }
}
