xquery version "3.1";

(:~
 : This module provides the editorial annotations for Ahiqar.
 :
 : @author Michelle Weidling
 : @version 1.9.0
 : @since 1.7.0
 : @see https://subugoe.pages.gwdg.de/ahiqar/api-documentation/
 : @see https://subugoe.pages.gwdg.de/ahiqar/api-documentation/page/text-api-specs/
 : :)

module namespace edit="http://ahiqar.uni-goettingen.de/ns/annotations/editorial";

declare namespace tei="http://www.tei-c.org/ns/1.0";

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

(:~
 : Gets the annotations for a given page for both possible text types
 : (transcription and transliteration, if present).
 :
 : At this stage, TEI files are scraped for person and place names.
 :
 : @param $teixml-uri The XML's URI.
 : @param $page The page within an XML file, i.e. a tei:pb/@n within a TEI resource
 :)
declare function edit:get-annotations($pages as element(page)+,
    $teixml-uri as xs:string)
as map()* {
    let $annotation-elements :=
        for $chunk in $pages return
            (: person and place names should only appear in the transcriptions, except
            for Karshuni texts where the transliteration is the source of truth.
            having the same elements twice (once in the "if" segment and once in the
            "else" segment) instead of extracting the once that both have in common
            is necessary to ensure that all annotations are displayed in the correct order. :)
            if ($chunk/@type = "transcription" and not($chunk/@lang eq "karshuni")
            or $chunk/@type = "transliteration" and $chunk/@lang eq "karshuni") then
                $chunk//(
                    tei:placeName |
                    tei:persName |
                    tei:add |
                    tei:del |
                    tei:damage |
                    tei:choice[not(parent::tei:damage)] |
                    tei:unclear |
                    tei:cit[not(@next)] | (: the cit elements that point to another have no original text in them and are therefore ignored :)
                    tei:surplus |
                    tei:seg[@type = "verse"] |
                    tei:catchwords |
                    tei:gap |
                    tei:supplied[not(ancestor::tei:choice)])
            else
                $chunk//(tei:add |
                tei:del |
                tei:damage |
                tei:choice[not(parent::tei:damage)] |
                tei:unclear |
                tei:cit[not(@next)] | (: the cit elements that point to another have no original text in them and are therefore ignored :)
                tei:surplus |
                tei:seg[@type = "verse"] |
                tei:catchwords |
                tei:gap |
                tei:supplied[not(ancestor::tei:choice)]
                )

    let $maps := for $annotation in $annotation-elements return edit:make-map($annotation, $teixml-uri)
    let $_annotation-values := for $map in $maps return string($map("body")("value"))
    let $annotation-values := distinct-values($_annotation-values)
    for $val in $annotation-values return
        let $relevant-annos := for $map in $maps return
            if ($map("body")("value") = $val) then
                $map
            else
                ()
        let $targets := for $anno in $relevant-annos return $anno("target") => array:get(1) => map:get("selector") => map:get("value")
        let $targets-string := string-join($targets, ",")
        (: when the value is equal, all of the other relevant information regarding
        an annotation is equal too. therefore we can just update the target of the
        first relevant annotation in the sequence.
        due to the nested structure we have to update everything :)
        let $selector-map := map:put($relevant-annos[1]("target") => array:get(1) => map:get("selector"), "value", $targets-string)
        let $target-array := array{map:put($relevant-annos[1]("target")  => array:get(1), "selector", $selector-map)}
        let $updated-anno := map:put($relevant-annos[1], "target", $target-array)
        return
            $updated-anno
};

declare function edit:make-map($annotation as element(),
    $teixml-uri as xs:string)
as map(*)? {
    (: if the annotation is referenced by a @corresp attribute, leave it out.
    this annotation has to be referred to by the referencing annotation, then. :)
    if ($annotation/@xml:id and $annotation/root()/descendant::*[@corresp = concat('#', $annotation/@xml:id)]) then
        ()
    else
        let $id := string( $annotation/@id ) (: get the predefined ID from the in-memory TEI with IDs :)
        return
            map {
                "id": $commons:anno-ns || "/" || $teixml-uri || "/annotation-" || $id,
                "type": "Annotation",
                "body": edit:get-body-object($annotation, $teixml-uri),
                "target": array { edit:get-target-information($annotation, $teixml-uri, $id) }
            }
};


(:~
 : Returns the Body Object for an annotation.
 :
 : @see https://www.w3.org/TR/annotation-model/#embedded-textual-body
 : @see https://subugoe.pages.gwdg.de/ahiqar/api-documentation/page/annotation-api-specs/#body-object
 :
 : @param $annotation The node which serves as a basis for the annotation
 : @param $teixml-uri The URI of the resp. XML document, e.g. "1234d"
 : @return A map representing the embedded textual body of the annotation.
 :)
declare function edit:get-body-object($annotation as node(),
    $teixml-uri as xs:string)
as map() {
    map {
        "type": "TextualBody",
        "value": edit:get-body-value($annotation, $teixml-uri),
        "format": "text/plain",
        "x-content-type": edit:get-annotation-type($annotation)
    }
};

declare function edit:make-annotation-text($annotation as node())
as xs:string? {
    (: annotations with @next are the first part of a name spanning over
    two lines. :)
    if ($annotation/@next) then
        let $first-part-text := $annotation//text() => string-join(" ")
        let $second-part-text := $annotation/id(substring-after($annotation/@next, "#"))//text() => string-join(" ")
        return $first-part-text || $second-part-text
    (: annotations with @xml:id are the second part of a name that is split
    over two lines. the second part has already been handled by the first
    part and can be savely ignored. :)
    else if ($annotation/@xml:id) then
        ()
    else if ($annotation[self::tei:persName and descendant::tei:corr]) then
        $annotation//tei:sic/text() => string-join(" ")
    else
        $annotation//text() => string-join(" ")
};

(:~
 : Returns the body's value for an annotation.
 :
 : @see https://www.w3.org/TR/annotation-model/#embedded-textual-body
 :
 : @param $annotation The node which serves as a basis for the annotation
 : @param $teixml-uri The URI of the resp. XML document, e.g. "1234d"
 : @return The value of the annotation.
 :)
declare function edit:get-body-value($annotation as node(),
    $teixml-uri as xs:string)
as xs:string {
    let $value :=
        typeswitch ( $annotation )
        case element(tei:persName) return
            edit:make-annotation-text($annotation)

        case element(tei:placeName) return
            edit:make-annotation-text($annotation)

        case element(tei:add) return
            if (string-join($annotation//text(), "") eq "") then
                ()
            else
                if ($annotation/@place) then
                    "An addition, place: " || $annotation/@place || ". Text: " || $annotation//text() => string-join(" ")
                else
                    "An addition. Text: " || $annotation//text() => string-join(" ")

        case element(tei:del) return
            "Text deleted by the scribe: " || $annotation//text() => string-join(" ")

        case element(tei:damage) return
            if ($annotation/tei:choice/tei:orig and $annotation/tei:choice/tei:supplied) then
                "A damaged passage. Original: <span dir='rtl'>" || $annotation//tei:orig || "</span>, supplied text: " || $annotation//tei:supplied
            else if ($annotation/tei:choice/tei:orig and $annotation/tei:choice/tei:corr) then
                "A damaged passage. Original: <span dir='rtl'>" || $annotation//tei:orig || "</span>, corrected text: " || $annotation//tei:corr
            else if ($annotation/@extent and not($annotation/text())) then
                "A damaged passage. Extent: " || $annotation/@extent
            else if ($annotation/text()[matches(., "[\w]")]) then
                "A damaged passage. Legible text: <span dir='rtl'>" || $annotation || "</span>"
            else if ($annotation/tei:g) then
                if ($annotation/tei:g[@type = "quotation-mark"]) then
                    "A damaged passage. Legible text: quotation mark"
                else
                    ()
            else if ($annotation/tei:supplied) then
                "A damaged passage. Legible text: <span dir='rtl'>" || string-join($annotation/tei:w/text(), " ") || "</span>, supplied text: " || $annotation/tei:supplied/text()
            else
                let $text := string-join($annotation/descendant::text(), " ")
                return
                    "A damaged passage. Legible text: <span dir='rtl'>" || $text || "</span"

        case element(tei:choice) return
            if($annotation/tei:sic) then
                let $resp :=
                    if ($annotation/tei:corr/@resp) then
                        let $id := if (contains($annotation/tei:corr/@resp, "#")) then
                            substring-after($annotation/tei:corr/@resp, "#")
                        else
                            $annotation/tei:corr/@resp
                        let $editor := doc($commons:data || $teixml-uri || ".xml")//tei:editor[@xml:id = $id]/text()
                        return "by " || $editor || " (editor)"
                    else
                        "by the scribe"
                let $strike-through := exists($annotation/tei:corr/tei:del)
                let $corrected-text :=
                    if ($strike-through) then
                        "<s>" || $annotation/tei:corr//text() => string-join(" ") || "</s>"
                    else
                        $annotation/tei:corr//text() => string-join(" ")
                return
                    "Correction of faulty text. Original: " || $annotation/tei:sic => string-join(" ") ||
                    ", corrected " || $resp || " to: " || $corrected-text
            else if($annotation/tei:supplied) then
                let $texts := for $suppl in $annotation/tei:supplied return
                    let $cert := if($suppl/@cert) then ". Certainty: " || $suppl/@cert else ()
                    return
                        if ($suppl/tei:unclear) then
                            "A supplied passage where the writing is unclear. Supplied text: <span dir='rtl'>" || $suppl/tei:unclear/text() || "</span>. Reason: " || replace($suppl/tei:unclear/@reason, "_", " ") || $cert
                        else
                            "Supplied text: <span dir='rtl'>" || $suppl/text() || "</span>" || $cert
                return string-join($texts, "<br/>")
            else
                "An abbreviation. Original: " || $annotation/tei:abbr => string-join(" ") || ". Expanded text: " || $annotation/tei:expan => string-join(" ")

        case element(tei:unclear) return
            if ($annotation/@reason) then
                "A passage where the writing cannot be fully deciphered. Text: <span dir='rtl'>" || $annotation || "</span>. Reason: " || replace($annotation/@reason, "_", " ")
            else
                "A passage where the writing cannot be fully deciphered. Text: <span dir='rtl'>" || $annotation || "</span>"

        case element(tei:cit) return
            if ($annotation/@type = 'verbatim' and $annotation/tei:bibl) then
                string-join($annotation//text(), " ") || ": a quote of " || ($annotation/tei:bibl => string-join(" "))
            else if ($annotation/@type = 'verbatim') then
                string-join($annotation//text(), " ") || ": a quote"
            else if ($annotation/@type = "paraphrase") then
                edit:make-paraphrase-text($annotation)

            else
                let $references-strings :=
                    for $note in $annotation/tei:note return
                        (($note/following-sibling::tei:bibl[1]//text() => string-join(" "))
                         || ": "
                         || ($note//text() => string-join(" ")))
                return
                    string-join($references-strings, "; &#10;")

        case element(tei:supplied) return
            let $editor := doc($commons:data || $teixml-uri || ".xml")//tei:titleStmt//tei:editor/text()

            return
            "A proposed addition by " || $editor || " (editor): <span dir='rtl'>" || string-join($annotation//text(), " ") || "</span>"

        case element(tei:surplus) return
            "Surplus text: " || string-join($annotation//text(), " ")

        case element(tei:quote) return
            "Quotation: " || string-join($annotation//tei:w, " ")

        case element(tei:seg) return
            "Inline verse: " || string-join($annotation//tei:w, " ")

        case element(tei:catchwords) return
            if ($annotation/tei:choice/tei:sic) then
                "Catchword(s): " || string-join($annotation//tei:sic/text(), " ")
            else if ($annotation//tei:choice/tei:expan) then
                "Catchword(s): " || string-join($annotation//tei:abbr/text(), " ")
            else
                "Catchword(s): " || string-join($annotation//text(), " ")

        case element(tei:gap) return
            "Text not reconstructible. " || $annotation/@extent || " " || $annotation/@unit || " missing. Reason: " || $annotation/@reason

        default return
            ()

    return
        (: normalize space of an empty sequence will return an empty string! :)
        normalize-space($value)
};


(: Creates the paraphrase text for all kinds of paraphrases (single vs. multiline). :)
declare function edit:make-paraphrase-text($cit as element(tei:cit))
as xs:string? {
    let $referencing-cit := edit:find-referencing-cit($cit)
    let $paraphrase-text :=
        (: multiline paraphrase :)
        if ($referencing-cit) then
            let $text-of-prev-lines := edit:find-prev-paraphrases-texts($cit, $referencing-cit)
            return string-join(($text-of-prev-lines, $cit/tei:quote//text()), " ")
        (: single line paraphrase :)
        else
            $cit/tei:quote//text() => string-join(" ")
    let $orig-phrases := $cit/tei:note[@type = "parallel-text"]/tei:note[@type = "original-phrase"]
    return
        if (count($orig-phrases) = 1) then
            let $orig-phrase := $cit/tei:note[@type = "parallel-text"]/tei:note[@type = "original-phrase"]//text()[not(parent::tei:expan)]
            let $orig := $cit/tei:note[@type = "parallel-text"]/tei:bibl[@type = "provided-by-editor"]//text()
            return
                $paraphrase-text || ': a paraphrase of ' || string-join($orig-phrase, " ") || ' (' || $orig || ')'
        else
            let $origs :=
                for $note in $orig-phrases return
                $note/text() || ' (' || $note/following-sibling::tei:bibl[1]//text() || ')'
            return
                $paraphrase-text || ': a paraphrase of the following parallel texts: ' || string-join($origs, ",<br>")
};

(: Returns the tei:cit that is referencing another.
 : Relevant for multiline paraphrases. :)
declare function edit:find-referencing-cit($cit as element(tei:cit))
as element(tei:cit)? {
    let $id := $cit/@xml:id
    return $cit/preceding::tei:cit["#" || $id = ./@next]
};

(: In case of a multiline paraphrase, returns all paraphrase texts that don't
 : have original text. :)
declare function edit:find-prev-paraphrases-texts($cit as element(tei:cit),
    $referencing-cit as element(tei:cit))
as xs:string {
    let $next-reference := edit:find-referencing-cit($referencing-cit)
    return
        if ($next-reference) then
            string-join((edit:find-prev-paraphrases-texts($referencing-cit, $next-reference),
                $referencing-cit/tei:quote//text()), " ")
        else
            $referencing-cit/tei:quote//text() => string-join(" ")
};

(: This function in necessary for getting the correct CSS values. :)
declare function edit:find-prev-paraphrases-ids($cit as element(tei:cit))
as xs:string {
    let $prev-paraphrase-part := edit:find-referencing-cit($cit)
    return
        if ($prev-paraphrase-part) then
            string-join((edit:find-prev-paraphrases-ids($prev-paraphrase-part), "#" || $cit/@id), ",")
        else
            "#" || $cit/@id
};

(:~
 : Returns the type of an annotation.
 :
 : @see https://www.w3.org/TR/annotation-model/#string-body
 :
 : @param $annotation The node which serves as a basis for the annotation
 : @return The content of bodyValue.
 :)
declare function edit:get-annotation-type($annotation as node())
as xs:string {
    switch ($annotation/local-name())
        case "persName" return "Person"
        case "placeName" return "Place"
        case "cit" return "Reference"
        default return "Editorial Comment"
};


(:~
 : Returns the target segment for an annotation.
 :
 : @param $annotation The node which serves as a basis for the annotation
 : @param $document-uri The resource's URI to which the $annotation belongs to
 : @param $id The node ID of the annotation. It is equivalent to generate-id($annotation)
 : @return A map containing the target information
 :)
declare function edit:get-target-information($annotation as node(),
    $document-uri as xs:string,
    $id as xs:string)
as map(*) {
    let $text-type :=
        if ($annotation/ancestor::tei:text[@xml:lang = 'karshuni']) then
            "transliteration"
        else
            "transcription"

    let $page := $annotation/preceding::tei:pb[1]/@n
    let $value := edit:make-css-target-values($annotation)
    return
        map {
        	"selector": map {
                "type": "CssSelector",
                "value": $value
            },
            "source": $commons:server || "/content/" || $text-type || "/" || $document-uri || "-" || commons:format-page-number($page) || ".html",
            "format": "text/xml",
            "language": $annotation/ancestor-or-self::*[@xml:lang][1]/@xml:lang/string()
        }
};

declare function edit:make-css-target-values($annotation as node())
as xs:string? {
    if ($annotation[self::tei:cit[@type = "paraphrase"]]) then
        edit:find-prev-paraphrases-ids($annotation)
    else
        if ($annotation/@next) then
            let $second-part-id := $annotation/following::*["#" || @xml:id = $annotation/@next]/@id
            return
                "#" || $annotation/@id || ",#" || $second-part-id
        else if ($annotation/@corresp and not($annotation[self::tei:note[@type = "original-phrase"]])) then
            let $referenced-id := $annotation/root()/descendant::*[@xml:id = substring-after($annotation/@corresp, "#")]/@id
            return
                "#" || $annotation/@id || ",#" || $referenced-id
        else if ($annotation/@xml:id) then
            ()
        else
            "#" || $annotation/@id
};
