xquery version "3.1";

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

declare namespace ore="http://www.openarchives.org/ore/terms/";
declare namespace rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
declare namespace tei="http://www.tei-c.org/ns/1.0";
declare namespace tgmd="http://textgrid.info/namespaces/metadata/core/2010";

import module namespace fragment="https://wiki.tei-c.org/index.php?title=Milestone-chunk.xquery" at "/db/apps/ahiqar/modules/fragment.xqm";
import module namespace me="http://ahiqar.uni-goettingen.de/ns/motifs-expansion" at "/db/apps/ahiqar/modules/motifs-expansion.xqm";
import module namespace tokenize="http://ahiqar.uni-goettingen.de/ns/tokenize" at "/db/apps/ahiqar/modules/tokenize.xqm";
import module namespace functx="http://www.functx.com" at "/db/system/repo/functx-1.0.1/functx/functx.xq";

declare variable $commons:expath-pkg := doc("../expath-pkg.xml");
declare variable $commons:version := $commons:expath-pkg/*/@version/string();
declare variable $commons:textapi-version := $commons:expath-pkg/*/@textapi-version/string();
declare variable $commons:tg-collection := "/db/data/textgrid";
declare variable $commons:data := $commons:tg-collection || "/data/";
declare variable $commons:meta := $commons:tg-collection || "/meta/";
declare variable $commons:agg := $commons:tg-collection || "/agg/";
declare variable $commons:tile := $commons:tg-collection || "/tile/";
declare variable $commons:json := $commons:tg-collection || "/json/";
declare variable $commons:html := $commons:tg-collection || "/html/";
declare variable $commons:tmp :=  $commons:tg-collection || "/tmp/";
declare variable $commons:appHome := $commons:expath-pkg/*/substring-before(base-uri(), "expath-pkg.xml");
declare variable $commons:api-prefix := "textapi/ahiqar/";
declare variable $commons:annotation-prefix := "annotations/ahiqar/";
declare variable $commons:path-to-vault-secret-file := "/vault/secrets/secret";

declare variable $commons:server := string($commons:expath-pkg/*/@api-base-url);

declare variable $commons:ns := "http://ahiqar.uni-goettingen.de/ns/commons";
declare variable $commons:anno-ns := "http://ahiqar.uni-goettingen.de/ns/annotations";

declare variable $commons:idno-to-sigils-map :=
    map {
        "162": "J",
        "430": "D",
        "433": "M",
        "Add_2020": "C",
        "Add_7200": "L",
        "Ar_7_229": "A",
        "Borg_ar_201": "Borg. ar. 201",
        "Brit_Libr_Or_9321": "Brit. Or. 9321",
        "Brit_Mus_Add_7209": "Brit. Add. 7209",
        "Camb_Add_2886": "Cam. Add. 2886",
        "Cambrigde_Add_3497": "Cam. Add. 3497",
        "CH_17": "Notre Dame University, Louaize CH 17",
        "Cod_Arab_236": "Cod. Arab. 236",
        "DFM_00614": "DFM 614",
        "GCAA_00486": "GCAA 486",
        "Manuscrit_4122": "T",
        "Mingana_Syr_258": "Ming. syr. 258",
        "Mingana_ar_christ_93_84": "Ming. ar. 93",
        "Mingana_syr_133": "Ming. syr. 133",
        "Ms_orient_A_2652": "Gotha 2652",
        "MS_SYR_21": "O",
        "MS_619": "Université Saint-Joseph. Bibliothèque orientale MS 619",
        "MS_621": "Université Saint-Joseph. Bibliothèque orientale MS 621",
        "MS_626": "Université Saint-Joseph. Bibliothèque orientale MS 626",
        "MS_882": "Church of the Forty Martyrs MS 882",
        "MS_956": "Ordre Basilien Alepin MS 956",
        "Or_1292b": "Leiden Or. 1292",
        "Or_2313": "R",
        "Paris_Arabe_3637": "Paris. ar. 3637",
        "Paris_Arabe_3656": "Paris. ar. 3656",
        "Sachau_162": "S",
        "Sachau_290_Sachau_339": "Sach. 339",
        "Sachau_336": "U",
        "Sado_Collection_ms_no_9": "P",
        "Salhani": "Salhani",
        "Sbath_25": "Sbath 25",
        "Syr_17": "Fondation Georges et Mathilde Salem Syr. 17",
        "Syr_80": "H",
        "Vat_ar_2054": "Vat. ar. 2054",
        "Vat_sir_159": "Vat. syr. 159",
        "Vat_sir_199": "Vat. syr. 199",
        "Vat_sir_424": "Vat. syr. 424",
        "ms_syr_434": "B",
        "syr_422": "N",
        "syr_611": "K",
        "syr_612": "I",
        "G": "G",
        "unknown": "unknown" (: likely a missing tei:idno :)
    };

declare variable $commons:idno-to-sigils-map-reverse :=
   ( map:keys($commons:idno-to-sigils-map) ! map:entry($commons:idno-to-sigils-map?(.) , .  ) )=> map:merge() ;

declare function commons:get-response-header($code as xs:integer)
as node() {
    <rest:response>
        <http:response xmlns:http="http://expath.org/ns/http-client" status="{$code}">
            <http:header name="Access-Control-Allow-Origin" value="*"/>
        </http:response>
    </rest:response>
};

declare function commons:get-response-header($code as xs:integer, $contentType as xs:string)
as node() {
    <rest:response>
        <http:response xmlns:http="http://expath.org/ns/http-client" status="{$code}">
            <http:header name="Access-Control-Allow-Origin" value="*"/>
            <http:header name="Content-Type" value="{$contentType}"/>
            <!-- Content-Type is stripped at parameter, so we can not deliver the full
                string. -->
            <http:header name="X-Content-Type" value="{$contentType}"/>
        </http:response>
    </rest:response>
};

(:~
 : Gets a secret with the given key from a file injected by vault
 : (kubernetes setup). Fallback: env var with the same key.
 : Requires dba role.
 :  :)
declare function commons:get-secret($key as xs:string) as xs:string {
    if (file:exists($commons:path-to-vault-secret-file))
    then
        (file:read($commons:path-to-vault-secret-file) => tokenize())[contains(., $key || "=")]
            => substring-after($key || "=")
    else
        if (available-environment-variables() = $key) then environment-variable($key)
        else
            error(QName($commons:ns, "authError"), "missing secret in either file or env: " || $key)

};

declare function commons:get-xml-uri($manifest-uri as xs:string)
as xs:string {
    let $aggregation-file := commons:get-document($manifest-uri, "agg")
    let $aggregates := $aggregation-file//ore:aggregates/@rdf:resource
    let $available-xmls :=
        for $file in $aggregates
        let $unprefixed := substring-after($file, "textgrid:")
        return
            if (doc-available(concat($commons:data, $unprefixed, ".xml"))) then
                $unprefixed
            else
                ()
    return
        if (count($available-xmls) eq 0) then
            error(QName($commons:ns, "COMMONS004"), "No available XML document found for the manifest " || $manifest-uri)
        else if (count($available-xmls) gt 1) then
            error(QName($commons:ns, "COMMONS005"), "Several available XML documents found for the manifest " || $manifest-uri || ": " || string-join($available-xmls, ", ") || ". Expected exactly one.")
        else
            $available-xmls
};

declare function commons:get-tei-xml-for-manifest($manifest-uri as xs:string)
as document-node() {
    let $xml-uri := commons:get-xml-uri($manifest-uri)
    return
        commons:get-document($xml-uri, "data")
};


declare function commons:get-document($uri as xs:string,
    $type as xs:string)
as document-node()? {
    let $collection :=
        switch ($type)
            case "agg" return $commons:agg
            case "data" return $commons:data
            case "meta" return $commons:meta
            default return error(QName($commons:ns, "COMMONS001"), "Unknown type " || $type)
    let $base-uri := $collection || $uri || ".xml"
    return
        if (doc-available($base-uri)) then
            doc($base-uri)
        else
            error(QName($commons:ns, "COMMONS002"), "URI " || $uri || " not found.")
};

declare function commons:get-available-aggregates($aggregation-uri as xs:string)
as xs:string* {
    let $aggregation-doc := commons:get-document($aggregation-uri, "agg")
    for $aggregate in $aggregation-doc//ore:aggregates/@rdf:resource
        let $unprefixed-uri := substring-after($aggregate, "textgrid:")
        let $aggregate-base-uri := $commons:meta || $unprefixed-uri || ".xml"
        return
            if (doc-available($aggregate-base-uri)) then
                $unprefixed-uri
            else
                ()
};

(:~
 : Return the parent aggregation of a given URI.
 :
 : @param $uri The resource's URI
 : @return The URI of the given resource's parent aggregation
 :)
declare function commons:get-parent-aggregation($uri as xs:string)
as xs:string? {
    let $parent := collection($commons:agg)//ore:aggregates[@rdf:resource = "textgrid:" || $uri]
    return
        $parent ! (.
            => base-uri()
            => substring-after("agg/")
            => substring-before(".xml"))
};

declare function commons:get-page-fragments($teixml-uri as xs:string,
    $page as xs:string)
as element()+ {
    let $nodeURI := commons:get-document($teixml-uri, "data")/base-uri()
    let $text-types := commons:get-text-types($teixml-uri)
    for $type in $text-types return
        commons:get-page-fragment($nodeURI, $page, $type)
};

declare function commons:get-text-types($teixml-uri as xs:string)
as xs:string+ {
    let $xml-doc := commons:open-tei-xml($teixml-uri)
    return
        $xml-doc//tei:text[@type]/string(@type)
};

declare function commons:get-languages($teixml-uri as xs:string)
as xs:string+ {
    let $xml-doc := commons:open-tei-xml($teixml-uri)
    return
        (: had an additional predicate: [@xml:lang = ("syc", "syc-syrj", "syc-syrn", "ara", "karshuni", "sog", "aii")]
         : what is obsolete when we take all langs for the given text types. :)
        $xml-doc//tei:text[@type = ("transcription", "transliteration")]/@xml:lang/string()
};


(:~
 : Returns a given page from a requested TEI document and from the requested text type.
 : In some cases the requested text type isn't available or doesn't have any text, so that
 : no page fragment can be retrieved.
 :
 : @param $tei-xml-base-uri The base URI of the requested TEI document
 : @param $page The page as given in tei:pb/@n
 : @param $text-type Either "transcription" or "transliteration"
 : @return The requested page in the resp. text type if available
 :)
declare function commons:get-page-fragment($tei-xml-base-uri as xs:string,
    $page as xs:string,
    $text-type as xs:string)
as element() {
    if (local:has-text-content($tei-xml-base-uri, $page, $text-type)) then
        let $uri := commons:get-uri-from-anything($tei-xml-base-uri)
        let $node-in-cache := doc-available($commons:tmp || $uri || ".me.xml")
        let $node :=
            if($node-in-cache) then
                doc($commons:tmp || $uri || ".me.xml")/*
            else
                let $result :=
                    doc($tei-xml-base-uri)/tei:TEI
                    => me:main()
                    => commons:add-IDs()
                    => tokenize:main()
                let $store := xmldb:store($commons:tmp, $uri || ".me.xml", $result)
                return
                    $result
        let $start-node-dry-run := $node//tei:text[@type = $text-type]//tei:pb[@n = $page],
            $start-node :=
            (:
                todo remove when motif expansion is aware of this.
                $start-node must contain a single node!
            :)
                if (count($start-node-dry-run) gt 1) then
                    $start-node-dry-run[1]
                else $start-node-dry-run,
            $end-node := commons:get-end-node($start-node),
            $wrap-in-first-common-ancestor-only := false(),
            $include-start-and-end-nodes := true(),
            $empty-ancestor-elements-to-include := ("")

        return
            fragment:get-fragment-from-doc(
                $node,
                $start-node,
                $end-node,
                $wrap-in-first-common-ancestor-only,
                $include-start-and-end-nodes,
                $empty-ancestor-elements-to-include)
    else
        ()
};

declare function local:has-text-content($tei-xml-base-uri as xs:string,
    $page as xs:string,
    $text-type as xs:string)
as xs:boolean {
    exists(doc($tei-xml-base-uri)/tei:TEI//tei:text[@type = $text-type]/descendant::tei:pb[@n = $page])
};

declare function commons:add-IDs($nodes as node()*)
as node()* {
    for $node in $nodes return
        typeswitch ($node)

        case text() return
            $node

        case comment() return
            ()

        case processing-instruction() return
            $node

        (: motifs are encoded as tei:span and get their IDs during the motif
        expansion process if they span more than one line. Therefore some tei:spans
        already have an ID while others don't. :)
        case element(tei:span) return
            element {QName("http://www.tei-c.org/ns/1.0", local-name($node))} {
                if ($node/@id) then
                    ()
                else
                    attribute id {commons:generate-id($node)},
                $node/@*,
                commons:add-IDs($node/node())
            }

        default return
            element {QName($node/namespace-uri(), local-name($node))} {
                attribute id {commons:generate-id($node)},
                $node/@*,
                commons:add-IDs($node/node())
            }
};

declare function commons:get-end-node($start-node as element(tei:pb))
as element() {
    let $ancestorText := $start-node/ancestor::tei:text[1]
    let $pbs          := $ancestorText//tei:pb
    let $following-pb := $pbs[. >> $start-node][1]
    return
        if($following-pb) then
            $following-pb
        else
            $start-node/following-sibling::*[last()]
};

declare function commons:get-metadata-file($uri as xs:string)
as document-node() {
    doc($commons:meta || $uri || ".xml")
};

declare function commons:get-aggregation($manifest-uri as xs:string)
as document-node() {
    doc($commons:agg || $manifest-uri || ".xml")
};

declare function commons:open-tei-xml($tei-xml-uri as xs:string)
as document-node() {
    let $uri := $commons:data || $tei-xml-uri || ".xml"
    return
        if(doc-available($uri)) then
            doc($commons:data || $tei-xml-uri || ".xml")
        else
            error(QName($commons:ns, "COMMONS003"), "Document not found: " || $uri)
};

(:~
 : Gets a currently valid or renewed session id from TextGrid
 : @return Session Id
:)
declare function commons:get-textgrid-session-id()
as xs:string {
    (: check if we have a session id :)
    if( util:binary-doc-available("/db/sid.txt") ) then
        (: check if we have to renew the session id :)
        if( current-dateTime() - xs:dayTimeDuration("PT23H55M") lt xmldb:last-modified("/db", "sid.txt")) then
            util:binary-doc("/db/sid.txt") => util:binary-to-string()
        else
            local:create-textgrid-session-id()
    else
        local:create-textgrid-session-id()

};

(:~
 : Gets a new session id from TextGrids WebAuth service and stores it to
 : binary /db/sid.txt
 : @return Session id
:)
declare %private function local:create-textgrid-session-id() {
    let $webauthUrl := "https://textgridlab.org/1.0/WebAuthN/TextGrid-WebAuth.php"
    let $authZinstance := "textgrid-esx2.gwdg.de"
    let $secret := commons:get-secret("TGLOGIN")

    let $user :=        $secret => substring-before(":")
    let $password :=    $secret => substring-after(":")

    let $pw :=
        if(contains($password, '&amp;')) then
            replace($password, '&amp;', '%26')
        else $password
    let $request :=
        <hc:request method="POST" href="{ $webauthUrl }" http-version="1.0">
            <hc:header name="Connection" value="close" />
            <hc:multipart media-type="multipart/form-data" boundary="------------------------{current-dateTime() => util:hash("md5") => substring(0,17)}">
                <hc:header name="Content-Disposition" value='form-data; name="authZinstance"'/>
                <hc:body media-type="text/plain">{$authZinstance}</hc:body>
                <hc:header name="Content-Disposition" value='form-data; name="loginname"'/>
                <hc:body media-type="text/plain">{$user}</hc:body>
                <hc:header name="Content-Disposition" value='form-data; name="password"'/>
                <hc:body media-type="text/plain">{$pw}</hc:body>
            </hc:multipart>
        </hc:request>
    let $response := hc:send-request($request)

    let $sid :=
        string($response[2]//*:meta[@name="rbac_sessionid"]/@content)

    let $sidTest :=
        if($sid = "") then
            error(QName($commons:ns, "authError"), $response[2])
        else ()

    let $store := xmldb:store-as-binary("/db", "sid.txt", $sid) => sm:chmod("rwxrwx---")

    return
        $sid

};

declare function commons:compress-to-zip($collection-uri as xs:string)
as xs:string* {
    if (commons:does-zip-need-update()) then
        let $valid-uris :=
            for $uri in xmldb:get-child-resources($collection-uri) return
                if (starts-with($uri, "syc")
                or starts-with($uri, "ara")
                or $uri eq "map.json"
                ) then
                    xs:anyURI($collection-uri || $uri)
                else
                    ()
        let $zip := compression:zip($valid-uris, false())
        return
            (
                commons:make-last-zip-created(),
                xmldb:store-as-binary("/db/data", "ahiqar-json.zip", $zip)
            )
    else
        ()
};

declare function commons:does-zip-need-update()
as xs:boolean {
    let $last-zip-created := commons:get-last-zip-created()
    let $latest-last-modified := commons:get-latest-lastModified()

    return
        if (not(exists($last-zip-created))
        or ($last-zip-created lt $latest-last-modified)) then
            true()
        else
            false()
};

declare function commons:make-last-zip-created() {
    let $contents :=
        <last-created>
            {current-dateTime()}
        </last-created>
    return
        xmldb:store("/db/data", "last-zip-created.xml", $contents)
};

declare function commons:get-last-zip-created()
as xs:dateTime? {
    xs:dateTime(doc("/db/data/last-zip-created.xml")/last-created)
};

declare function commons:get-latest-lastModified()
as xs:dateTime {
    let $last-modifieds := collection($commons:meta)//tgmd:lastModified
    let $sorted-modifieds :=
        for $date in $last-modifieds
        order by $date descending
        return
            $date
    return
        $sorted-modifieds[1]
};


(:~
 : Returns all page break numbers for a given TEI resource.
 :
 : @param $documentURI The TEI resource's URI
 : @return A sequence of all page breaks occuring in the resource
 :)
declare function commons:get-pages-in-TEI($uri as xs:string)
as xs:string+ {
    let $page-ids := commons:get-document($uri, "data")//tei:pb[@facs and ancestor::tei:text[@type = "transcription"]]/@n/string()
    for $id in $page-ids return
        commons:format-page-number($id)
};

declare function commons:get-pages-for-text-type($uri as xs:string,
    $type as xs:string) {
    commons:get-document($uri, "data")//tei:text[@type = $type]//tei:pb/@n/string()
};

declare function commons:format-page-number($pb as xs:string) {
    replace($pb, " ", "")
    => replace("[^a-zA-Z0-9]", "")
};

declare function commons:extract-uri-from-base-uri($base-uri as xs:string) {
    functx:substring-after-last($base-uri, "/")
    => substring-before(".xml")
};

(:~ Gets the URI from base-uri() and any textgrid uri
 : $anyUriForm – accepts base-uri(), textgrid URI and URI (= textgrid
 : base URI without prefix)
 : @return URI :)
declare function commons:get-uri-from-anything($anyUriForm as xs:string) {
    if(contains($anyUriForm, "/") and ends-with($anyUriForm, ".xml")) then
        $anyUriForm => commons:extract-uri-from-base-uri()
    else if (starts-with($anyUriForm, "textgrid:") and contains($anyUriForm, ".")) then
        $anyUriForm => substring-after(":") => substring-before(".")
    else if (starts-with($anyUriForm, "textgrid:")) then
        $anyUriForm => substring-after(":")
    else
        $anyUriForm
};

(:~
 : Gets the parent URI of a resource.
 : Note: in textgrid a URI can be present in multiple aggregations. Currently
 : this feature is not used for ahiqar.
 : @param $uri – URI (textgrid base URI without prefix)
 : @return URI of the parent aggregation :)
declare function commons:get-parent-uri($uri as xs:string)
as xs:string* {
    collection($commons:agg)//ore:aggregates[@rdf:resource eq "textgrid:" || $uri]/base-uri()
    ! commons:extract-uri-from-base-uri(.) (: simple map operator to filter for empty-sequence :)
};

(:~
 : Gets the child TEI URI for a manifest/edition.
 : @param $uri – URI (textgrid base URI without prefix)
 : @return URI of the parent aggregation :)
declare function commons:get-child-tei-uri($uri as xs:string)
as xs:string* {
    for $aggregates in collection($commons:agg)
        //rdf:Description[matches(@rdf:about, "textgrid:" || $uri || "\.[0-9]+")]
        /ore:aggregates/substring-after(@rdf:resource, "textgrid:")
    let $docUri := $commons:data || $aggregates || ".xml"
    where doc-available($docUri) and exists(doc($docUri)/tei:TEI)
    return
        $docUri
};


(:~
 : Prepares a metadata map for a resource/collection
 : @param $uri – URI (textgrid base URI without prefix)
 : @return A map with keys: title, textgrid-uri, uri, format :)
declare function commons:get-resource-information($uri as xs:string) {
    let $metadata := doc($commons:meta || $uri || ".xml")
    let $format := string($metadata//tgmd:format)
    let $parent := commons:get-parent-uri($uri)
    return
        map:merge((
        map{
            "title": string($metadata//tgmd:title),
            "textgrid-uri": string($metadata//tgmd:textgridUri),
            "uri": commons:get-uri-from-anything(string($metadata//tgmd:textgridUri)),
            "format": $format,
            "parents": $parent,
            "languageCollection": commons:get-language-collection-name($uri)
        },
        if ($format = "text/xml") then
            map{"manifestIndexNumber": index-of(commons:get-available-aggregates(commons:get-parent-uri($parent)), $parent) - 1}
        else ()
        ))
};

(:~
 : The path to a resource according to TextGrid schema.
 : Empty sequence if no parent aggregation is present in the database.
 :  :)
declare function commons:get-path($uri as xs:string) as xs:string* {
    let $parent := commons:get-parent-uri($uri)
    return ($parent, $parent ! commons:get-path(.))
};

(:~
 : Return the language aggregation name a resource belongs to
 : in the textgrid aggregatin path.
 :   :)
declare function commons:get-language-collection-name($uri as xs:string) as xs:string* {
    let $path := commons:get-path($uri)
    return
        if ("3r132" = $path) then
            let $langUri := $path[last() - 1]
            return
                string(doc($commons:meta || $langUri || ".xml")//tgmd:title)
        else
            ()
};

declare function commons:get-parent-collection-information($uri as xs:string)
as map(*){
    commons:get-parent-uri($uri)
    => commons:get-resource-information()
};

declare function commons:validate-rest-input($collection-type as xs:string,
    $manifest-uri as xs:string?, $page as xs:string?)
as item()? {
    (: collections :)
    if ($collection-type = ("syriac", "arabic", "karshuni", "neo-aramaic", "sogdian")) then
        ()
    else
        error(QName($commons:ns, "INPUT_VALIDATION_01"), "Unknown collection " || $collection-type),

    (: manifests :)
    if ($manifest-uri and doc-available($commons:agg || $manifest-uri || ".xml")) then
        ()
    else if (not($manifest-uri)) then
        ()
    else
        error(QName($commons:ns, "INPUT_VALIDATION_02"), "Unknown manifest " || $manifest-uri),

    (: items :)
    (
        if ($page) then
            let $tei-uri := commons:get-child-tei-uri($manifest-uri)
                => commons:get-uri-from-anything()
            let $pages-in-tei := commons:get-pages-in-TEI($tei-uri)
            return
                if (not($page)) then
                    ()
                else if( doc-available($commons:data || $tei-uri || ".xml")
                and $page = $pages-in-tei) then
                    ()
                else
                    error(QName($commons:ns, "INPUT_VALIDATION_03"), "Unknown page " || $page || " for manifest " || $manifest-uri)
        else
            ()
    )
};

(:~
    This function generates the ID prefix for tokenization. The result is
    used to identify the source document in CollateX.
    Generated values have to match $commons:idno-to-sigils-map.
:)
declare function commons:make-id-from-idno($TEI as element(tei:TEI))
as xs:string {
    let $idno := $TEI//tei:sourceDesc//tei:msIdentifier/tei:idno
    let $normalized :=
        replace($idno, "[\+,\.]", "")
        => replace("[\(\)=\[\]\\]", " ")
        => replace("/", " ")
        => normalize-space()
        => replace(" ", "_")
    return
        (: in some cases the idno doesn't start with a letter but a digit.
        to get a uniform ID we prepend a prefix for all tokens. :)
        "t_" || $normalized
};

declare function commons:get-transcription-and-transliteration-per-page($teixml-uri as xs:string,
    $page as xs:string)
as element(tei:TEI)+ {
    let $xml-doc := commons:open-tei-xml($teixml-uri)
    let $langs := $xml-doc//tei:text[@xml:lang[. = ("syc", "ara", "karshuni")]]/@xml:lang/string()
    return
        if ($langs = "karshuni") then
            (commons:get-page-fragment-from-uri($teixml-uri, $page, "transcription"),
            commons:get-page-fragment-from-uri($teixml-uri, $page, "transliteration"))
        else
            commons:get-page-fragment-from-uri($teixml-uri, $page, "transcription")
};


(:~
 : Returns a single page from a TEI resource, i.e. all content from the given $page
 : up to the next page break.
 :
 : @param $documentURI The resource's URI
 : @param $page The page to be returned as tei:pb/@n/string()
 :)
declare function commons:get-page-fragment-from-uri($documentURI as xs:string,
    $page as xs:string,
    $text-type as xs:string)
as element(tei:TEI)? {
    let $nodeURI := commons:get-document($documentURI, "data")/base-uri()
    return
        commons:get-page-fragment($nodeURI, $page, $text-type)
};

(:~
 : Creates a unique and stable ID for an element or attribute node.
 : This function is necessary because the built-in generate-id() uses a seed
 : which leads to different IDs on consecutive executions. This impedes the
 : lookup e.g. for the variants.
 :)
declare function commons:generate-id($node as node())
as xs:string? {
    let $base-value := 'N' || (generate-id($node) => substring-after('N'))
    return
        if ($node instance of attribute()) then
            $base-value || "_" || local-name($node)
        else if ($node instance of element()) then
            $base-value
        else
            ()

};

(:~
    This is a generic debug function placed here, bc this module is imported
    to most other modules. So we can share debug functionality like logging
    memory.
:)
declare function commons:debug($message as xs:string) {
    let $unit-converter := function($bytes) {
        let $unit :=
            if($bytes > math:pow(1024, 3)) then
                (math:pow(1024, 3), "GB")
            else if($bytes > math:pow(1024, 2)) then
                (math:pow(1024, 2), "MB")
            else
                (1024, "KB")
        return
            format-number($bytes div $unit[1], ".00") || " " || $unit[2]
        }

    let $maxRaw := system:get-memory-max()
    let $max := $unit-converter($maxRaw)
    let $allocatedInUseRaw := system:get-memory-total()- system:get-memory-free()
    let $allocatedInUse := $unit-converter($allocatedInUseRaw)
    let $allocatedFree := $unit-converter(system:get-memory-free())
    let $allocatedTotal := $unit-converter(system:get-memory-total())
    let $available := $unit-converter(system:get-memory-max()- system:get-memory-total()- system:get-memory-free())

    let $color :=
        (: turn color from green (0,255,0) to red (255,0,0) :)
        function($value, $max) {
            let $relation := $value div $max
            let $r := xs:int((255 * $relation))
            let $g := 255 - $r
            let $b := 0
            return
                " &#27;[48;2;" || $r || ";" || $g || ";" || $b || "m"
        }


    return
    (: print debug log only in critial memory usage over 50% :)
    if (($allocatedInUseRaw div $maxRaw) lt 0.5) then () else
        util:log-system-out(
            $message || $color($allocatedInUseRaw, $maxRaw)
            || " " || $allocatedInUse
            || " of maximum " || $max || " &#27;[0m"
        )
};

declare function commons:get-valid-page-ids($manifest-uri as xs:string)
as xs:string+ {
    let $tei-xml := commons:get-tei-xml-for-manifest($manifest-uri)
    return
        $tei-xml//tei:text[@type="transcription"]//tei:pb[@facs]/string(@n)
};
