<?php

namespace CorpusParole\Http\Controllers\Sparql;

use Log;

use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;

use GuzzleHttp\Client;
use EasyRdf\Sparql\Result as SparqlResult;
use EasyRdf\Graph;
use EasyRdf\RdfNamespace;

use CorpusParole\Http\Controllers\Controller;
use CorpusParole\Libraries\Utils;

use CorpusParole\Libraries\Sparql\SparqlClient;
use CorpusParole\Libraries\Sparql\SparqlQueryAnalyser;
use RdfHelper;

class SparqlClientController extends Controller
{

    const HEADERS_FORWARDED = [ "host", "user-agent", "accept", "accept-language", "accept-encoding", "connection" ];

    const SELECT_RESULT_FORMAT = [
        "SPARQL/CSV" => "text/csv",
        "SPARQL/JSON" => "application/sparql-results+json",
        "SPARQL/XML" => "application/sparql-results+xml",
        "SPARQL/TSV" => "text/tab-separated-values",
        "BINARY" => "application/x-binary-rdf-results-table"
    ];

    const GRAPH_RESULT_FORMAT = [
        "N-Triples" => "application/n-triples",
        "RDF/XML" => "application/rdf+xml",
        "Turtle" => "text/turtle",
        "N3" => "text/n3",
        "RDF/JSON" => "application/rdf+json",
        "TriG" => "application/trig",
        "N-Quads" => "application/n-quads",
        "BinaryRDF" => "application/x-binary-rdf",
        "TriX" => "application/trix",
        "JSON-LD" => "application/ld+json"
    ];


    private $sparqlClient;
    private $httpClient;

    public function __construct(SparqlClient $sparqlClient, Client $httpClient) {
        $this->sparqlClient = $sparqlClient;
        $this->httpClient = $httpClient;
    }

    public function getSparqlClient($timeout = null) {
        if(is_null($timeout)) {
            $timeout = config('corpusparole.sparql_client_timeout');
        }
        $queryUrl = config('corpusparole.rdf4j_query_url');
        if($timeout > 0) {
            $queryUrl = $queryUrl .
                ((strlen(parse_url($queryUrl, PHP_URL_QUERY)) > 0)?"&":"?").
                "timeout=$timeout";
        }
        return new \CorpusParole\Libraries\Sparql\GuzzleSparqlClient($this->httpClient, $queryUrl, config('corpusparole.rdf4j_update_url'));
    }

    // display form
    public function index() {
        return view('sparql/sparqlClientForm');
    }

    private function readDocs($docs, $fields) {
        $results = [];
        foreach($docs as $row) {
            $results[] = array_reduce($fields, function($res, $field) use ($row) {
                if(isset($row->$field)) {
                    $res[$field] = RdfHelper::serialiseValue($row->$field);
                } else {
                    $res[$field] = "<span class=\"corpus-rdf-blank-value\">&nbsp;</span>";
                }
                return $res;
            }, []);
        }
        return $results;
    }

    private function abort($code, $message, $exception) {
        throw new \Symfony\Component\HttpKernel\Exception\HttpException($code, $message, $exception, []);
    }

    private function processQueryException($exception) {
        $message = $exception->getMessage();
        if($exception instanceof \EasyRdf\Http\Exception) {
            if(preg_match("/SPARQL\squery/", $message)) {
                $this->abort(400, "La requête SPARQL n'est pas reconnue", $exception);
            } else {
                $this->abort(500, "Problème HTTP lors de la requête SPARQL", $exception);
            }
        } elseif($exception instanceof \EasyRdf\Exception) {
            if(preg_match("/timed\sout/i", $message)) {
                $this->abort(408, "Time-out causé par la requête SPARQL", $exception);
            } else {
                $this->abort(500, "Problème dans la requête SPARQL", $exception);
            }
        } else {
            $this->abort(500, "Erreur serveur lors de la requête", $exception);
        }
    }

    private function querySelect(Request $request, $query, $analyser) {

        $limit = intval($request->input('limit', config('corpusparole.sparql_client_default_limit')));

        if($limit === 0 || !is_null($analyser->getLimit()) || !is_null($analyser->getOffset()) ) {
            try {
                $docs = $this->getSparqlClient()->query($query);
            } catch(\Exception $exception) {
                $this->processQueryException($exception);
            }

            $fields = $docs->getFields();
            $results = $this->readDocs($docs, $fields);
            $count = count($results);

        } else {

            $page = Paginator::resolveCurrentPage(config('corpusparole.pagination_page_param'));
            assert(is_null($page) || is_numeric($page));

            $offset = max(0,($page - 1) * $limit);

            try {
                $countResult = $this->getSparqlClient()->query($analyser->getCountQuery());
                $docs = $this->getSparqlClient()->query($query . " LIMIT $limit OFFSET $offset");
            } catch(\Exception $exception) {
                $this->processQueryException($exception);
            }


            $countField = $countResult->getFields()[0];
            $countResult->rewind();
            $count = $countResult->current()->$countField->getValue();

            $fields = $docs->getFields();

            $results = new LengthAwarePaginator($this->readDocs($docs, $fields), $count, $limit, $page, [
                'path' => Paginator::resolveCurrentPath(),
                'pageName' => config('corpusparole.pagination_page_param'),
            ]);
        }

        $namespaces = array_reduce(array_keys(RdfHelper::getPrefixes()), function($res, $p) {
            $res[$p] = RdfNamespace::namespaces()[$p];
            return $res;
        }, []);

        $data = [
            'query' => $query,
            'count' => $count,
            'fields' => $fields,
            'fieldPrefix' => "?",
            'results' => $results,
            'namespaces' => $namespaces,
            'downloadFormats' => self::SELECT_RESULT_FORMAT
        ];
        $view = 'sparql/sparqlClientResultList';
        return [$view, $data];
    }

    private function queryGraph(Request $request, $query, $analyser) {
        try {
            $docs = $this->getSparqlClient()->query($query);
        } catch(\Exception $exception) {
            $this->processQueryException($exception);
        }


        $fields = ["subject", "predicate", "object"];
        $results = [];
        foreach ($docs->resources() as $resource ) {
            foreach ($resource->propertyUris() as $property) {
                $propertyResource = $docs->resource($property);
                foreach ($resource->all($propertyResource) as $value) {
                    $results[] = [
                        'subject' => RdfHelper::serialiseValue($resource),
                        'predicate'=> RdfHelper::serialiseValue($propertyResource),
                        'object'=> RdfHelper::serialiseValue($value)
                    ];
                }
            }
        }
        $namespaces = array_reduce(array_keys(RdfHelper::getPrefixes()), function($res, $p) {
            $res[$p] = RdfNamespace::namespaces()[$p];
            return $res;
        }, []);


        $data = [
            'query' => $query,
            'count' => count($results),
            'fields' => $fields,
            'fieldPrefix' => "",
            'results' => $results,
            'namespaces' => $namespaces,
            'downloadFormats' => self::GRAPH_RESULT_FORMAT
        ];
        $view = 'sparql/sparqlClientResultList';

        return [$view, $data];
    }

    private function queryAsk(Request $request, $query, $analyser) {
        try {
            $result = $this->getSparqlClient()->query($query);
        } catch(\Exception $exception) {
            $this->processQueryException($exception);
        }

        $data = [
            'results' => $result,
            'namespaces' => $analyser->getPrefixes()
        ];

        $view = 'sparql/sparqlClientResultBoolean';
        return [$view, $data];
    }

    private function showHtml(Request $request) {

        $query = $request->input('query');

        $analyser = new SparqlQueryAnalyser($query);

        $queryType = $analyser->getQueryType();

        $namespaces = $analyser->getPrefixes();

        foreach($namespaces as $prefix => $nUri) {
            RdfNamespace::set($prefix,$nUri);
        }

        if($queryType === SparqlQueryAnalyser::SELECT_QUERY) {
            list($view, $data) = $this->querySelect($request, $query, $analyser);
        } elseif($queryType === SparqlQueryAnalyser::GRAPH_QUERY) {
            list($view, $data) = $this->queryGraph($request, $query, $analyser);
        } elseif($queryType === SparqlQueryAnalyser::ASK_QUERY) {
            list($view, $data) = $this->queryAsk($request, $query, $analyser);
        } else {
            abort(400, "La requête n'est pas reconnue");
        }

        return view($view, $data);

    }


    private function proxyQuery(Request $request, $format=null) {
        $query = $request->input('query');
        $headers = [];
        foreach (self::HEADERS_FORWARDED as $h) {
            $headerValue = $request->header($h);
            if($headerValue) {
                $headers[$h] = $headerValue;
            }
        }

        if(!empty($format)){
            $headers['Accept'] = $format;
        }

        $queryParams = $request->all();
        $queryParams['timeout'] = config('corpusparole.sparql_client_timeout');
        $queryUrl = config('corpusparole.rdf4j_query_url');

        try {
            $rdf4jResp = $this->httpClient->post($queryUrl, ['form_params' => $queryParams, 'headers' => $headers]);
        } catch(\GuzzleHttp\Exception\ServerException $exception) {
            if($exception->getCode() == 503) {
                $this->abort(408, "Time-out causé par la requête SPARQL", $exception);
            } else {
                $this->abort(500, "Problème lors de la requête SPARQL", $exception);
            }

        } catch(\GuzzleHttp\Exception\RequestException $exception) {
            $message = $exception->getMessage();
            if(preg_match("/MALFORMED\sQUERY/i", $message)) {
                $abortMessage = "Requête SPARQL mal-formée";
            } else {
                $abortMessage = "Problème lors de la requête SPARQL";
            }
            $this->abort($exception->getCode(), $abortMessage, $exception);
        } catch(\Exception $exception) {
            $this->abort(500, "Erreur serveur lors de la requête", $exception);
        }

        $resp = response((string)$rdf4jResp->getBody(), $rdf4jResp->getStatusCode());
        foreach ($rdf4jResp->getHeaders() as $name => $values) {
            if($name != 'Transfer-Encoding') {
                $resp->header($name, $values);
            }
        }

        return $resp;
    }

    // display result
    public function show(Request $request) {

        $format = $request->input('format');

        if($format === 'text/html') {
            return $this->showHtml($request);
        } else {
            return $this->proxyQuery($request, $format);
        }

    }

    // do the query
    public function query(Request $request) {
        return $this->proxyQuery($request);
    }

}
