xquery version "3.1";

(:~
 : This module provides the TextAPI for Ahiqar. For an extensive explanation of
 : the whole Ahiqar specific implementation of the generic TextAPI see the link
 : to the API docs.
 :
 : @author Mathias Göbel
 : @author Michelle Weidling
 : @since 0.0.0
 : @see https://subugoe.pages.gwdg.de/ahiqar/api-documentation/page/text-api-specs/
 : :)

module namespace tapi="http://ahiqar.uni-goettingen.de/ns/tapi";

declare namespace output="http://www.w3.org/2010/xslt-xquery-serialization";
declare namespace tei="http://www.tei-c.org/ns/1.0";
declare namespace test="http://exist-db.org/xquery/xqsuite";
declare namespace xhtml="http://www.w3.org/1999/xhtml";
declare namespace http = "http://expath.org/ns/http-client";

import module namespace tapi-coll="http://ahiqar.uni-goettingen.de/ns/tapi/collection" at "tapi-collection.xqm";
import module namespace tapi-item="http://ahiqar.uni-goettingen.de/ns/tapi/item" at "tapi-item.xqm";
import module namespace tapi-mani="http://ahiqar.uni-goettingen.de/ns/tapi/manifest" at "tapi-manifest.xqm";
import module namespace commons="http://ahiqar.uni-goettingen.de/ns/commons" at "commons.xqm";
import module namespace requestr="http://exquery.org/ns/request";
import module namespace rest="http://exquery.org/ns/restxq";
import module namespace tei2html="http://ahiqar.uni-goettingen.de/ns/tei2html" at "tei2html.xqm";
import module namespace tei2json="http://ahiqar.uni-goettingen.de/ns/tei2json" at "tei2json.xqm";
import module namespace tapi-html="http://ahiqar.uni-goettingen.de/ns/tapi/html" at "tapi-html.xqm";
import module namespace head="http://ahiqar.uni-goettingen.de/ns/http-headers" at "/db/apps/ahiqar/modules/http-headers.xqm";


(:~
 : Shows information about the currently installed application.
 :
 : @return The content of the app's expath-pkg.xml and repo.xml as JSON
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/info")
    %output:method("json")
function tapi:endpoint-info()
as item()+ {
    commons:get-response-header(200),
    tapi:info()
};

(:~
 : Retrieves info about the Ahiqar app.
 :
 : @return element(descriptors)
 :)
declare function tapi:info()
as element(descriptors) {
    <descriptors>
        <request>
            <scheme>{ requestr:scheme() }</scheme>
            <hostname>{ requestr:hostname() }</hostname>
            <uri>{ requestr:uri() }</uri>
        </request>
        {
            tapi:remove-whitespaces(doc($commons:appHome || "/expath-pkg.xml")),
            tapi:remove-whitespaces(doc($commons:appHome || "/repo.xml"))
        }
    </descriptors>
};

(:~
 : Removes all line breaks and surplus white spaces from XML files.
 : This way we avoid producing a littered JSON with fields that only contain
 : white space text.
 :
 : @author Michelle Weidling
 : @param $doc The XML document to be transformed
 : @return $doc but without indentation
 :)
declare function tapi:remove-whitespaces($doc as document-node()) as document-node() {
    $doc => serialize() => replace("[\s]{2,}", "") => replace("[\n]", "") => parse-xml()
};


(:~
 : @see https://subugoe.pages.gwdg.de/emo/text-api/page/specs/#collection
 : @see https://subugoe.pages.gwdg.de/emo/text-api/page/specs/#collection-object
 : @param $collection-type The collection type as defined in `commons:validate-rest-input`
 : @return A collection object as JSON
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/ahiqar/{$collection-type}/collection.json")
    %output:method("json")
function tapi:endpoint-collection($collection-type as xs:string)
as item()+ {
    try {
        commons:get-response-header(200),
        tapi-coll:get-json($collection-type)
    }
    catch Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_01 {
        commons:get-response-header(404),
        $err:code || ": " || $err:description
    }
    catch * {
        local:make-error-response($err:code, $err:description,
            $err:line-number, $err:column-number,
            "An error occurred while processing " || $collection-type)
    }
};


(:~
 : @see https://subugoe.pages.gwdg.de/emo/text-api/page/specs/#manifest
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/ahiqar/{$collection-type}/{$manifest-uri}/manifest.json")
    %output:method("json")
function tapi:endpoint-manifest($collection-type as xs:string,
    $manifest-uri as xs:string)
as item()+ {
    try {
        commons:get-response-header(200),
        tapi-mani:get-json($collection-type, $manifest-uri, $commons:server)
    }
    catch Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_01 |
        Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_02 {
        commons:get-response-header(404),
        $err:code || ": " || $err:description
    }
    catch * {
        local:make-error-response($err:code, $err:description,
            $err:line-number, $err:column-number,
            "An error occurred while processing " || $manifest-uri)
    }
};


(:~
 : Returns information about a given page in a document. This is mainly compliant
 : with the SUB TextAPI, but has the following additions:
 :  * the division number, 'n', is mandatory
 :  * 'image' is mandatory since every page has a facsimile
 :
 : Sample call to API: /textapi/ahiqar/syriac/3r1pq/147a/latest/item.json
 :
 : @see https://subugoe.pages.gwdg.de/emo/text-api/page/specs/#item
 : @param $collection-type The collection type as defined in `commons:validate-rest-input`
 : @param $manifest-uri The unprefixed TextGrid URI of a document, e.g. '3r1pq'
 : @param $page A page number as encoded in a tei:pb/@n, e.g. '147a'
 : @return Information about a page
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/ahiqar/{$collection-type}/{$manifest-uri}/{$page}/latest/item.json")
    %output:method("json")
function tapi:endpoint-item($collection-type as xs:string,
    $manifest-uri as xs:string,
    $page as xs:string)
as item()+ {
    try {
        commons:get-response-header(200),
        tapi-item:get-json($collection-type, $manifest-uri, $page, $commons:server)
    }
    catch Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_01 |
        Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_02 |
        Q{http://ahiqar.uni-goettingen.de/ns/commons}INPUT_VALIDATION_03 {
        commons:get-response-header(404),
        $err:code || ": " || $err:description
    }
    catch * {
        local:make-error-response($err:code, $err:description,
            $err:line-number, $err:column-number,
            "An error occurred while processing " || $manifest-uri)
    }
};

(:~
 : Returns an HTML rendering of a given page.
 :
 : Since we only return a fragment of an HTML page which can be integrated in a
 : viewer or the like, we chose XML as output method in order to avoid setting
 : a DOCTYPE on a fragment.
 :
 : Sample call to API: /content/3rbmb-1a.html
 :
 : @param $text-type The text type ("transcription" or "transliteration")
 : @param $tei-xml-uri The unprefixed TextGrid URI of a TEI/XML, e.g. '3rbmb'
 : @param $page The page to be rendered. This has to be the string value of a tei:pb/@n in the given document, e.g. '1a'
 : @return A response header as well as the rendered HTML page
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/content/{$text-type}/{$tei-xml-uri}-{$page}.html")
    %output:method("xhtml")
    %output:indent("no")
function tapi:endpoint-html($tei-xml-uri as xs:string,
    $text-type as xs:string,
    $page as xs:string)
as item()+ {
    let $doc-path := $commons:html || $tei-xml-uri || "-" || commons:format-page-number($page) || "-" || $text-type || ".html"
    return
        if (doc-available($doc-path)) then
            let $doc := doc($commons:html || $tei-xml-uri || "-" || commons:format-page-number($page) || "-" || $text-type || ".html")/*
            (: in case of the oral testimonies we set a variable for the TextAPI URL while
               creating the HTML files. The TextAPI URL depends on the server on which the
               application is deployed which can only be determined on runtime.
               This we correct this var here sind the API call is always performed at
               runtime. :)
            let $corrected-src := serialize($doc) => replace("AHIQAR_TEXTAPI_URL", environment-variable("AHIQAR_TEXTAPI_URL")) => parse-xml()
            return
            (
                (: the produced content type is stripped at parameter delimiter :)
                commons:get-response-header(200, "text/xhtml+xml;charset=UTF-8;type=" || $text-type),
                $corrected-src/*
            )
        else
            (
                commons:get-response-header(404),
                "Couldn't find document " || $tei-xml-uri || ", page " || $page || " for text type " || $text-type || " at " || $doc-path
            )
};

(:~
 : Returns an image belonging to a given URI. This function doesn't work locally
 : unless you have all necessary login information filled in at ahiqar.env.
 :
 : Since these images of the Ahiqar project aren't publicly available, this
 : function cannot be tested by unit tests.
 :
 : @param $availability-flag either `public` or `restricted`
 : @param $uri The unprefixed TextGrid URI of an image, e.g. '3r1pr'
 : @return The image as binary
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/images/{$availability-flag}/{$uri}")
    %rest:produces("image/jpeg")
    %output:method("binary")
function tapi:endpoint-restricted-image($availability-flag as xs:string,
    $uri as xs:string)
as item()+ {
    local:make-image-request($availability-flag, $uri, "")
};

(:~
 : Returns an image section belonging to a given URI as defined by the $image-section
 : paramater.
 : This function doesn't work locally unless you have all necessary login
 : information filled in at ahiqar.env.
 :
 : Since the images of the Ahiqar project aren't publicly available, this
 : function cannot be tested by unit tests.
 :
 : @param $availability-flag either `public` or `restricted`
 : @param $uri The unprefixed TextGrid URI of an image, e.g. '3r1pr'
 : @param $image-section Indicates the image section in percentage to be retured as defined by
 : the IIIF Image API
 : @return The image as binary
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/images/{$availability-flag}/{$uri}/{$image-section}")
    %rest:produces("image/jpeg")
    %output:method("binary")
function tapi:endpoint-image($availability-flag as xs:string,
    $uri as xs:string,
    $image-section as xs:string)
as item()+ {
    local:make-image-request($availability-flag, $uri, $image-section)
};

(:~
 : Endpoint to deliver all texts as JSON in a zip container. This comes in handy
 : e.g. for applications doing text analysis.
 :
 : @return The response header as well as a xs:base64Binary (the ZIP file)
 :)
declare
    %rest:GET
    %rest:HEAD
    %rest:path("/content/ahiqar-json.zip")
    %output:method("binary")
function tapi:endpoint-json() as item()+ {
    if (util:binary-doc-available("/db/data/ahiqar-json.zip")) then
            (
                commons:get-response-header(200),
                util:binary-doc("/db/data/ahiqar-json.zip")
            )
    else
        try {
            tei2json:main(),
            try {
                commons:compress-to-zip($commons:json),
                commons:get-response-header(200),
                util:binary-doc("/db/data/ahiqar-json.zip")
            } catch * {
                local:make-error-response($err:code, $err:description,
                    $err:line-number, $err:column-number,
                    "An error occurred while compressing the files to ZIP")
            }
        } catch * {
                local:make-error-response($err:code, $err:description,
                    $err:line-number, $err:column-number,
                    "An error occurred while creating the JSON files for ZIP")
        }
};


declare
    %rest:GET
    %rest:HEAD
    %rest:path("/content/{$font}.woff")
    %output:media-type("font/woff")
    %output:method("binary")
function tapi:endpoint-fonts($font as xs:string) as item()+ {
    if (util:binary-doc-available(concat("/db/data/resources/fonts/", $font, ".woff"))) then
        (
            commons:get-response-header(200),
            util:binary-doc(concat("/db/data/resources/fonts/", $font, ".woff"))
        )
    else
        (
            commons:get-response-header(404),
            "Unable to find font " || $font || ".woff"
        )
};

declare
    %rest:GET
    %rest:HEAD
    %rest:path("/content/{$filename}.mp3")
    %output:method("binary")
    %output:media-type("audio/mp3")
function tapi:endpoint-audio($filename as xs:string) as item()+ {
    if (util:binary-doc-available(concat("/db/data/resources/mp3/", $filename, ".mp3"))) then
        (
            commons:get-response-header(200),
            util:binary-doc(concat("/db/data/resources/mp3/", $filename, ".mp3"))
        )
    else
        (
            commons:get-response-header(404),
            "Unable to find file " || $filename || ".mp3"
        )
};

declare
    %rest:GET
    %rest:HEAD
    %rest:path("/content/ahiqar.css")
    %output:method("text")
    %output:media-type("text/css")
function tapi:endpoint-css() as item()+ {
    if (util:binary-doc-available("/db/data/resources/css/ahiqar.css")) then
        (
            commons:get-response-header(200),
            util:binary-doc("/db/data/resources/css/ahiqar.css")
            => util:base64-decode()
        )
    else
        (
            commons:get-response-header(404),
            "Unable to find ahiqar.css"
        )
};


(:~
 : Requests image data depending on the parameters passed.
 :
 : Sample calls processed by this function include:
 : * /image/public/12345
 : * /image/restricted/6789/50.03,0.48,49.83,100.00
 :
 :)
declare function local:make-image-request($availability-flag as xs:string,
    $uri as xs:string,
    $image-section as xs:string?)
as item()+ {
    if ($availability-flag = ("public", "restricted")) then
        let $sessionID :=
            if ($availability-flag = "restricted") then
                ";sid=" || commons:get-textgrid-session-id()
            else
                (: as soon as the public images have been published in the
                TextGrid Repository, we won't need a session ID for them. In
                the meantime the session ID is still necessary. :)
(:                "":)
                ";sid=" || commons:get-textgrid-session-id()
        let $section :=
            if ($image-section) then
                "/pct:" || $image-section
            else
                "/full"
        return
            (
                commons:get-response-header(200),
                try {
                    hc:send-request(
                        <hc:request method="GET"
                        href="https://textgridlab.org/1.0/digilib/rest/IIIF/textgrid:{$uri}{$sessionID}{$section}/,2000/0/native.jpg"
                        />
                    )[2] => xs:base64Binary()
                } catch * {
                    error(QName("http://ahiqar.uni-goettingen.de/ns/tapi", "TAPI01"), "The requested image with the URI " || $uri || " could not be fetched.")
                }
            )
    else
        <rest:response>
            <http:response xmlns:http="http://expath.org/ns/http-client" status="404">
                <http:header name="Access-Control-Allow-Origin" value="*"/>
            </http:response>
        </rest:response>
};

declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/http-status-test/collection/collection.json")
    %output:method("json")
function tapi:http-endpoint-collection()
as item()+ {
    commons:get-response-header(200),
    head:get-collection()
};

declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/http-status-test/collection/manifest/manifest.json")
    %output:method("json")
function tapi:http-endpoint-manifest()
as item()+ {
    commons:get-response-header(200),
    head:get-manifest()
};

declare
    %rest:GET
    %rest:HEAD
    %rest:path("/textapi/http-status-test/collection/manifest/{$http-status}/latest/item.json")
    %output:method("json")
function tapi:http-endpoint-item($http-status as xs:string)
as item()+ {
    <rest:response>
        <http:response xmlns:http="http://expath.org/ns/http-client" status="{$http-status}">
            <http:header name="Access-Control-Allow-Origin" value="*"/>
        </http:response>
    </rest:response>,
    "This is the end point for the status code " || $http-status
};

declare function local:make-error-response($code as xs:QName, $desc as xs:string,
    $line as xs:integer, $column as xs:integer, $rest-message as xs:string?) {
    commons:get-response-header(500),
    $rest-message,
    concat("[", $line, ": ", $column,
        "] Error ", $code, ": ", $desc)
};
