#######################################################################
# lfl: Linguistic Fuzzy Logic
# Copyright (C) 2025 Michal Burda
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#######################################################################


.elementWisely <- function(f) {
    function(...) {
        elts <- list(...)
        if (length(elts) <= 0L) {
            return(NULL)
        }
        vals <- lapply(elts, as.numeric)
        res <- do.call('mapply', c(list(f), vals))
        mostattributes(res) <- attributes(elts[[1L]])
        return(res)
    }
}


.algebraModification <- function(call, algebra, norm, conorm, resid, neg, invol, ord) {
    resN <- neg(algebra$n)
    resNI <- invol(algebra$ni)
    resT <- norm(algebra$t)
    resPT <- .elementWisely(resT)
    resC <- conorm(algebra$c)
    resR <- resid(algebra$r)
    resI <- norm(algebra$i)
    resPI <- .elementWisely(resI)
    resS <- conorm(algebra$s)
    resB <- function(x, y) { resPI(resR(x, y), resR(y, x)) }
    res <- list(n=resN,
                ni=resNI,
                t=resT,
                pt=resPT,
                c=resC,
                pc=.elementWisely(resC),
                r=resR,
                b=resB,
                i=resI,
                pi=resPI,
                s=resS,
                ps=.elementWisely(resS),
                order=ord,
                algebratype=c(algebra$algebratype, call))
    class(res) <- c('algebra', 'list')
    res
}


.neg0 <- function(f) {
    return(function(x) {
        res <- f(x)
        res[is.na(x)] <- 0
        res
    })
}


.neg1 <- function(f) {
    return(function(x) {
        res <- f(x)
        res[is.na(x)] <- 1
        res
    })
}


.negNA <- function(f) {
    return(function(x) {
        res <- f(x)
        res[is.na(x)] <- NA_real_
        res
    })
}


.normKleene <- function(f) {
    return(function(...) {
        dots <- c(...)
        nonadots <- na.omit(dots)
        res <- f(nonadots)
        if (length(dots) != length(nonadots) && !is.na(res) && res > 0) {
            return(NA_real_)
        }
        res
    })
}


.conormKleene <- function(f) {
    return(function(...) {
        dots <- c(...)
        nonadots <- na.omit(dots)
        res <- f(nonadots)
        if (length(dots) != length(nonadots) && !is.na(res) && res < 1) {
            return(NA_real_)
        }
        res
    })
}


.conormDragon <- function(f) {
    return(function(...) {
        dots <- c(...)
        nonadots <- na.omit(dots)
        res <- f(nonadots)
        if (length(dots) != length(nonadots) && !is.na(res) && res == 0) {
            return(NA_real_)
        }
        res
    })
}


.normUpper <- function(f) {
    return(function(...) {
        dots <- c(...)
        nonadots <- na.omit(dots)
        res <- f(nonadots)
        if (length(dots) != length(nonadots) && !is.na(res) && res == 1) {
            return(NA_real_)
        }
        res
    })
}


.undefinedOrder <- function(x, decreasing=FALSE) {
    stop('Cannot complete the computation, "order" is unimplemented for the algebra.')
}


.dragonflyOrder <- function(x, decreasing=FALSE) {
    large <- sum(!is.na(x) & x > 0)
    zeros <- sum(!is.na(x) & x == 0)
    nas <- sum(is.na(x))
    ordered <- order(x, decreasing=TRUE, na.last=TRUE)
    res <- c(ordered[seq(from=1, length.out=large)],
             ordered[seq(from=large + zeros + 1, length.out=nas)],
             ordered[seq(from=large + 1, length.out=zeros)])
    if (decreasing) {
        return(res)
    } else {
        return(rev(res))
    }
}


.upperBoundOrder <- function(x, decreasing=FALSE) {
    small <- sum(!is.na(x) & x < 1)
    ones <- sum(!is.na(x) & x == 1)
    nas <- sum(is.na(x))
    ordered <- order(x, decreasing=TRUE, na.last=TRUE)
    res <- c(ordered[seq(from=1, length.out=ones)],
             ordered[seq(from=ones + small + 1, length.out=nas)],
             ordered[seq(from=ones + 1, length.out=small)])
    if (decreasing) {
        return(res)
    } else {
        return(rev(res))
    }
}


#' Modify algebra's way of computing with `NA` values.
#'
#' By default, the objects created with the [algebra()] function represent a mathematical
#' algebra capable to work on the \eqn{[0,1]} interval. If `NA` appears as a value instead,
#' it is propagated to the result. That is, any operation with `NA` results in `NA`, by default.
#' This scheme of handling missing values is also known as Bochvar's. To change this default
#' behavior, the following functions may be applied.
#'
#' The `sobocinski()`, `kleene()`, `nelson()`, `dragonfly()`, `lowerEst()` and `lowerBound()`
#'  functions modify the algebra to
#' handle the `NA` in a different way than is the default. Sobocinski's algebra simply ignores `NA` values
#' whereas Kleene's algebra treats `NA` as "unknown value". Dragonfly approach is a combination
#' of Sobocinski's and Bochvar's approach, which preserves the ordering `0 <= NA <= 1`
#' to obtain from compositions (see [compose()])
#' the lower-estimate in the presence of missing values.
#'
#' In detail, the behavior of the algebra modifiers is defined as follows:
#'
#' Sobocinski's negation for `n` being the underlying algebra:
#' \tabular{ll}{
#'   a  \tab n(a)\cr
#'   NA \tab 0
#' }
#'
#' Sobocinski's operation for `op` being one of `t`, `pt`, `c`, `pc`, `i`, `pi`, `s`, `ps`
#' from the underlying algebra:
#' \tabular{lll}{
#'      \tab b        \tab NA\cr
#'   a  \tab op(a, b) \tab a \cr
#'   NA \tab b        \tab NA
#' }
#'
#' Sobocinski's operation for `r` from the underlying algebra:
#' \tabular{lll}{
#'       \tab b       \tab NA  \cr
#'    a  \tab r(a, b) \tab n(a)\cr
#'    NA \tab b       \tab NA
#' }
#'
#' Kleene's negation is identical to `n` from the underlying algebra.
#'
#' Kleene's operation for `op` being one of `t`, `pt`, `i`, `pi` from the underlying algebra:
#' \tabular{llll}{
#'      \tab b        \tab NA \tab 0\cr
#'   a  \tab op(a, b) \tab NA \tab 0\cr
#'   NA \tab NA       \tab NA \tab 0\cr
#'   0  \tab 0        \tab 0  \tab 0
#' }
#'
#' Kleene's operation for `op` being one of `c`, `pc`, `s`, `ps` from the underlying algebra:
#' \tabular{llll}{
#'      \tab b        \tab NA \tab 1\cr
#'   a  \tab op(a, b) \tab NA \tab 1\cr
#'   NA \tab NA       \tab NA \tab 1\cr
#'   1  \tab 1        \tab 1  \tab 1
#' }
#'
#' Kleene's operation for `r` from the underlying algebra:
#' \tabular{llll}{
#'      \tab b        \tab NA \tab 1\cr
#'   a  \tab r(a, b)  \tab NA \tab 1\cr
#'   NA \tab NA       \tab NA \tab 1\cr
#'   0  \tab 1        \tab 1  \tab 1
#' }
#'
#' Dragonfly negation is identical to `n` from the underlying algebra.
#'
#' Dragonfly operation for `op` being one of `t`, `pt`, `i`, `pi` from the underlying algebra:
#' \tabular{lllll}{
#'      \tab b        \tab NA \tab 0 \tab 1\cr
#'   a  \tab op(a, b) \tab NA \tab 0 \tab a\cr
#'   NA \tab NA       \tab NA \tab 0 \tab NA\cr
#'   0  \tab 0        \tab 0  \tab 0 \tab 0\cr
#'   1  \tab b        \tab NA \tab 0 \tab 1
#' }
#'
#' Dragonfly operation for `op` being one of `c`, `pc`, `s`, `ps` from the underlying algebra:
#' \tabular{lllll}{
#'      \tab b        \tab NA \tab 0  \tab 1\cr
#'   a  \tab op(a, b) \tab a  \tab a  \tab 1\cr
#'   NA \tab b        \tab NA \tab NA \tab 1\cr
#'   0  \tab b        \tab NA \tab 0  \tab 1\cr
#'   1  \tab 1        \tab 1  \tab 1  \tab 1
#' }
#'
#' Dragonfly operation for `r` from the underlying algebra:
#' \tabular{lllll}{
#'      \tab b       \tab NA \tab 0    \tab 1\cr
#'   a  \tab r(a, b) \tab NA \tab n(a) \tab 1\cr
#'   NA \tab b       \tab 1  \tab NA   \tab 1\cr
#'   0  \tab 1       \tab 1  \tab 1    \tab 1\cr
#'   1  \tab b       \tab NA \tab 0    \tab 1
#' }
#'
#' LowerEst and LowerBound algebras are equivalent to Dragonfly's algebra in
#' `t`, `pt`, `i`, `pi`, `c`, `pc`, `s`, `ps` operations, but differ in the
#' `r` and `n` operations. `n` is equal to the Sobocinski's negation for both
#' LowerEst and LowerBound algebras.
#'
#' LowerEst's operation for `r` from the underlying algebra:
#'
#' \tabular{lllll}{
#'      \tab b       \tab NA \tab 0    \tab 1\cr
#'   a  \tab r(a, b) \tab NA \tab n(a) \tab 1\cr
#'   NA \tab b       \tab NA \tab 0    \tab 1\cr
#'   0  \tab 1       \tab 1  \tab 1    \tab 1\cr
#'   1  \tab b       \tab NA \tab 0    \tab 1
#' }
#'
#' LowerBound's operation for `r` from the underlying algebra:
#'
#' \tabular{lllll}{
#'      \tab b       \tab NA \tab 0    \tab 1\cr
#'   a  \tab r(a, b) \tab NA \tab n(a) \tab 1\cr
#'   NA \tab 1       \tab 1  \tab 0    \tab 1\cr
#'   0  \tab 1       \tab 1  \tab 1    \tab 1\cr
#'   1  \tab b       \tab NA \tab 0    \tab 1
#' }
#'
#' @param algebra the underlying algebra object to be modified -- see the [algebra()] function
#' @return A list of function of the same structure as is the list returned from the [algebra()] function
#' @author Michal Burda
#' @keywords models robust
#' @examples
#' a <- algebra('lukas')
#' b <- sobocinski(a)
#'
#' a$t(0.3, NA)  # NA
#' b$t(0.3, NA)  # 0.3
#'
#' @export
#' @rdname algebraNA
#' @aliases algebraNA
sobocinski <- function(algebra) {
    .mustBeAlgebra(algebra)

    norm <- function(f) {
        return(function(...) {
            nonadots <- na.omit(c(...))
            if (length(nonadots) <= 0) {
                return(NA_real_)
            }
            f(nonadots)
        })
    }

    resid <- function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            res[yNA] <- algebra$n(x[yNA])
            res[xNA] <- y[xNA]
            res[xNA & yNA] <- NA_real_
            res
        })
    }

    .algebraModification('sobocinski', algebra, norm, norm, resid, .neg0, .negNA, .undefinedOrder)
}


#' @export
#' @rdname algebraNA
kleene <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <- function(f) {
        return(function(x, y) {
            res <- f(x, y)
            res[is.na(x)] <- NA_real_
            res[is.na(y)] <- NA_real_
            res[x == 0] <- 1
            res[y == 1] <- 1
            res
        })
    }

    .algebraModification('kleene', algebra, .normKleene, .conormKleene, resid, .negNA, .negNA, .undefinedOrder)
}


#' @export
#' @rdname algebraNA
dragonfly <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <-function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            x0 <- !xNA & x == 0
            y0 <- !yNA & y == 0
            res[xNA] <- y[xNA]
            res[yNA] <- NA_real_
            res[xNA & yNA] <- 1
            res[yNA & x0] <- 1
            res[xNA & y0] <- NA_real_
            return(res)
        })
    }

    alg <- .algebraModification('dragonfly', algebra, .normKleene, .conormDragon, resid, .negNA, .negNA, .dragonflyOrder)
    return(alg)
}


#' @export
#' @rdname algebraNA
nelson <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <- function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            x0 <- !xNA & x == 0
            y0 <- !yNA & y == 0
            y1 <- !yNA & y == 1
            res[yNA] <- NA_real_
            res[x0 & yNA] <- 1
            res[xNA] <- 1
            res[xNA & !(y0 | y1 | yNA)] <- NA_real_
            res
        })
    }

    .algebraModification('nelson', algebra, .normKleene, .conormKleene, resid, .neg1, .negNA, .undefinedOrder)
}


#' @export
#' @rdname algebraNA
lowerEst <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <-function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            x0 <- !xNA & x == 0
            res[yNA] <- NA_real_
            res[yNA & x0] <- 1
            res[xNA] <- y[xNA]
            res
        })
    }

    alg <- .algebraModification('lowerEst', algebra, .normKleene, .conormDragon, resid, .neg0, .negNA, .dragonflyOrder)
    return(alg)
}


#' @export
#' @rdname algebraNA
lowerBound <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <-function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            x0 <- !xNA & x == 0
            y0 <- !yNA & y == 0
            res[yNA] <- NA_real_
            res[xNA] <- 1
            res[xNA & y0] <- 0
            res[x0 & yNA] <- 1
            res
        })
    }

    alg <- .algebraModification('lowerBound', algebra, .normKleene, .conormDragon, resid, .neg0, .negNA, .dragonflyOrder)
    return(alg)
}


#' @export
#' @rdname algebraNA
upperBound <- function(algebra) {
    .mustBeAlgebra(algebra)

    resid <-function(f) {
        return(function(x, y) {
            res <- f(x, y)
            xNA <- is.na(x)
            yNA <- is.na(y)
            x1 <- !xNA & x == 1
            y1 <- !yNA & y == 1
            res[xNA] <- y[xNA]
            res[yNA] <- 1
            res[x1 & yNA] <- NA_real_
            res
        })
    }

    alg <- .algebraModification('upperBound', algebra, .normUpper, .conormKleene, resid, .neg0, .negNA, .upperBoundOrder)
    return(alg)
}
