/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.fuseki.servlets;

import static java.lang.String.format ;
import static org.apache.jena.fuseki.servlets.ServletBase.errorBadRequest ;
import static org.apache.jena.fuseki.servlets.ServletBase.errorOccurred ;
import static org.apache.jena.fuseki.servlets.ServletBase.log ;

import java.io.IOException ;
import java.util.HashMap ;
import java.util.Map ;
import java.util.Objects;

import javax.servlet.ServletOutputStream ;
import javax.servlet.http.HttpServletRequest ;
import javax.servlet.http.HttpServletResponse ;

import org.apache.commons.lang.StringUtils ;
import org.apache.jena.atlas.web.AcceptList ;
import org.apache.jena.atlas.web.MediaType ;
import org.apache.jena.fuseki.DEF ;
import org.apache.jena.fuseki.FusekiException ;
import org.apache.jena.fuseki.conneg.ConNeg ;
import org.apache.jena.query.QueryCancelledException ;
import org.apache.jena.query.ResultSet ;
import org.apache.jena.query.ResultSetFormatter ;
import org.apache.jena.riot.ResultSetMgr ;
import org.apache.jena.riot.WebContent ;
import org.apache.jena.riot.resultset.ResultSetLang ;
import org.apache.jena.sparql.core.Prologue ;
import org.apache.jena.web.HttpSC ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** This is the content negotiation for each kind of SPARQL query result */ 
public class ResponseResultSet
{
    private static Logger xlog = LoggerFactory.getLogger(ResponseResultSet.class) ;
    private static Logger slog = ServletBase.log ;

    // Short names for "output="
    private static final String contentOutputJSON          = "json" ;
    private static final String contentOutputXML           = "xml" ;
    private static final String contentOutputSPARQL        = "sparql" ;
    private static final String contentOutputText          = "text" ;
    private static final String contentOutputCSV           = "csv" ;
    private static final String contentOutputTSV           = "tsv" ;
    private static final String contentOutputThrift        = "thrift" ;
    
    public static Map<String,String> shortNamesResultSet = new HashMap<>() ;
    static {
        // Some short names.  keys are lowercase.
        ResponseOps.put(shortNamesResultSet, contentOutputJSON,   WebContent.contentTypeResultsJSON) ;
        ResponseOps.put(shortNamesResultSet, contentOutputSPARQL, WebContent.contentTypeResultsXML) ;
        ResponseOps.put(shortNamesResultSet, contentOutputXML,    WebContent.contentTypeResultsXML) ;
        ResponseOps.put(shortNamesResultSet, contentOutputText,   WebContent.contentTypeTextPlain) ;
        ResponseOps.put(shortNamesResultSet, contentOutputCSV,    WebContent.contentTypeTextCSV) ;
        ResponseOps.put(shortNamesResultSet, contentOutputTSV,    WebContent.contentTypeTextTSV) ;
        ResponseOps.put(shortNamesResultSet, contentOutputThrift, WebContent.contentTypeResultsThrift) ;
    }
    
    interface OutputContent { void output(ServletOutputStream out) ; }

    public static void doResponseResultSet(HttpAction action, Boolean booleanResult)
    {
        doResponseResultSet$(action, null, booleanResult, null, DEF.rsOfferTable) ;
    }

    public static void doResponseResultSet(HttpAction action, ResultSet resultSet, Prologue qPrologue)
    {
        doResponseResultSet$(action, resultSet, null, qPrologue, DEF.rsOfferTable) ;
    }
    
    // If we refactor the conneg into a single function, we can split boolean and result set handling. 
    
    // One or the other argument must be null
    private static void doResponseResultSet$(HttpAction action,
                                             ResultSet resultSet, Boolean booleanResult, 
                                             Prologue qPrologue, 
                                             AcceptList contentTypeOffer) 
    {
        HttpServletRequest request = action.request ;
        HttpServletResponse response = action.response ;
        long id = action.id ;
        
        if ( resultSet == null && booleanResult == null )
        {
            xlog.warn("doResponseResult: Both result set and boolean result are null") ; 
            throw new FusekiException("Both result set and boolean result are null") ;
        }
        
        if ( resultSet != null && booleanResult != null )
        {
            xlog.warn("doResponseResult: Both result set and boolean result are set") ; 
            throw new FusekiException("Both result set and boolean result are set") ;
        }

        String mimeType = null ; 
        MediaType i = ConNeg.chooseContentType(request, contentTypeOffer, DEF.acceptRSXML) ;
        if ( i != null )
            mimeType = i.getContentType() ;
        
        // Override content type
        // Does &output= override?
        // Requested output type by the web form or &output= in the request.
        String outputField = ResponseOps.paramOutput(request, shortNamesResultSet) ;    // Expands short names
        if ( outputField != null )
            mimeType = outputField ;
        
        String serializationType = mimeType ;           // Choose the serializer based on this.
        String contentType = mimeType ;                 // Set the HTTP respose header to this.
             
        // Stylesheet - change to application/xml.
        final String stylesheetURL = ResponseOps.paramStylesheet(request) ;
        if ( stylesheetURL != null && Objects.equals(serializationType,WebContent.contentTypeResultsXML) )
            contentType = WebContent.contentTypeXML ;
        
        // Force to text/plain?
        String forceAccept = ResponseOps.paramForceAccept(request) ;
        if ( forceAccept != null )
            contentType = WebContent.contentTypeTextPlain ;

        // Better : dispatch on MediaType
        // Fuseki2 uses the SPARQL parser/write registry.
        if ( Objects.equals(serializationType, WebContent.contentTypeResultsXML) )
            sparqlXMLOutput(action, contentType, resultSet, stylesheetURL, booleanResult) ;
        else if ( Objects.equals(serializationType, WebContent.contentTypeResultsJSON) )
            jsonOutput(action, contentType, resultSet, booleanResult) ;
        else if ( Objects.equals(serializationType, WebContent.contentTypeTextPlain) )
            textOutput(action, contentType, resultSet, qPrologue, booleanResult) ;
        else if ( Objects.equals(serializationType, WebContent.contentTypeTextCSV) ) 
            csvOutput(action, contentType, resultSet, booleanResult) ;
        else if (Objects.equals(serializationType, WebContent.contentTypeTextTSV) )
            tsvOutput(action, contentType, resultSet, booleanResult) ;
        else if (Objects.equals(serializationType, WebContent.contentTypeResultsThrift) )
            thriftOutput(action, contentType, resultSet, booleanResult) ;
        else
            errorBadRequest("Can't determine output serialization: "+serializationType) ;
    }
    
    
    public static void setHttpResponse(HttpServletRequest httpRequest,
                                       HttpServletResponse httpResponse,
                                       String contentType, String charset) 
    {
        // ---- Set up HTTP Response
        // Stop caching (not that ?queryString URLs are cached anyway)
        if ( true )
        {
            httpResponse.setHeader("Cache-Control", "no-cache") ;
            httpResponse.setHeader("Pragma", "no-cache") ;
        }
        // See: http://www.w3.org/International/O-HTTP-charset.html
        if ( contentType != null )
        {
            if ( charset != null && ! isXML(contentType) )
                contentType = contentType+"; charset="+charset ;
            log.trace("Content-Type for response: "+contentType) ;
            httpResponse.setContentType(contentType) ;
        }
    }

    private static boolean isXML(String contentType)
    {
        return contentType.equals(WebContent.contentTypeRDFXML)
            || contentType.equals(WebContent.contentTypeResultsXML)
            || contentType.equals(WebContent.contentTypeXML) ; 
    }

    private static void sparqlXMLOutput(HttpAction action, String contentType, final ResultSet resultSet, final String stylesheetURL, final Boolean booleanResult)
    {
        OutputContent proc = 
            new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetFormatter.outputAsXML(out, resultSet, stylesheetURL) ;
                if ( booleanResult != null )
                    ResultSetFormatter.outputAsXML(out, booleanResult, stylesheetURL) ;
            }} ;
            output(action, contentType, null, proc) ;
        }
    
    private static void jsonOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult)
    {
        OutputContent proc = new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetFormatter.outputAsJSON(out, resultSet) ;
                if (  booleanResult != null )
                    ResultSetFormatter.outputAsJSON(out, booleanResult ) ;
            }
        } ;
        
        try {
            String callback = ResponseOps.paramCallback(action.request) ;
            ServletOutputStream out = action.response.getOutputStream() ;

            if ( callback != null )
            {
                callback = StringUtils.replaceChars(callback, "\r", "") ;
                callback = StringUtils.replaceChars(callback, "\n", "") ;
                out.print(callback) ;
                out.println("(") ;
            }

            output(action, contentType, WebContent.charsetUTF8, proc) ;

            if ( callback != null )
                out.println(")") ;
        } catch (IOException ex) { errorOccurred(ex) ; }
    }
    
    private static void textOutput(HttpAction action, String contentType, final ResultSet resultSet, final Prologue qPrologue, final Boolean booleanResult)
    {
        // Text is not streaming.
        OutputContent proc =  new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetFormatter.out(out, resultSet, qPrologue) ;
                if (  booleanResult != null )
                    ResultSetFormatter.out(out, booleanResult ) ;
            }
        };

        output(action, contentType, WebContent.charsetUTF8, proc) ;
    }

    private static void csvOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) {
        OutputContent proc = new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetFormatter.outputAsCSV(out, resultSet) ;
                if (  booleanResult != null )
                    ResultSetFormatter.outputAsCSV(out, booleanResult ) ;
            }
        } ;
        output(action, contentType, WebContent.charsetUTF8, proc) ; 
    }

    private static void tsvOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) {
        OutputContent proc = new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetFormatter.outputAsTSV(out, resultSet) ;
                if (  booleanResult != null )
                    ResultSetFormatter.outputAsTSV(out, booleanResult ) ;
            }
        } ;
        output(action, contentType, WebContent.charsetUTF8, proc) ; 
    }

    private static void thriftOutput(HttpAction action, String contentType, final ResultSet resultSet, final Boolean booleanResult) {
        OutputContent proc = new OutputContent(){
            @Override
            public void output(ServletOutputStream out)
            {
                if ( resultSet != null )
                    ResultSetMgr.write(out, resultSet, ResultSetLang.SPARQLResultSetThrift) ;
                if ( booleanResult != null )
                    slog.error("Can't write boolen result in thrift") ;
            }
        } ;
        output(action, contentType, WebContent.charsetUTF8, proc) ; 
    }

    private static void output(HttpAction action, String contentType, String charset, OutputContent proc) 
    {
        try {
            setHttpResponse(action.request, action.response, contentType, charset) ; 
            action.response.setStatus(HttpSC.OK_200) ;
            ServletOutputStream out = action.response.getOutputStream() ;
            try
            {
                proc.output(out) ;
                out.flush() ;
            } catch (QueryCancelledException ex) {
                // Bother.  Status code 200 already sent.
                slog.info(format("[%d] Query Cancelled - results truncated (but 200 already sent)", action.id)) ;
                out.println() ;
                out.println("##  Query cancelled due to timeout during execution   ##") ;
                out.println("##  ****          Incomplete results           ****   ##") ;
                out.flush() ;
                // No point raising an exception - 200 was sent already.  
                //errorOccurred(ex) ;
            }
        // Includes client gone.
        } catch (IOException ex) 
        { errorOccurred(ex) ; }
        // Do not call httpResponse.flushBuffer(); here - Jetty closes the stream if it is a gzip stream
        // then the JSON callback closing details can't be added. 
    }
}
