#' @name L1centMDS
#' @title Fitting a Target Plot
#'
#' @description
#' \code{L1centMDS()} and \code{plot.L1centMDS()} are used together to draw a
#' target plot, which is a target-shaped 2D plot that aids in the visual
#' inspection of an undirected graph using the
#' \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}} centrality. See Kang
#' and Oh (2026) for a formal definition of a target plot.
#'
#' @details
#' Denoting the \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}}
#' centrality of vertex \eqn{i} as \eqn{c_i\in(0,1]}, a point representing that vertex is placed
#' on a concentric circle with radius \eqn{r_i =
#' -\log(c_i)}. Representing each vertex as \eqn{(r_i, \theta_i)} (in circular
#' coordinates), the values of \eqn{\theta_i} are derived using nonmetric
#' multidimensional scaling proposed in Kruskal (1964a,b). The initial
#' configuration is derived using classical multidimensional scaling
#' ([stats::cmdscale()]). A gradient descent algorithm is employed in deriving
#' optimal \eqn{\theta_i}s.
#'
#' @note
#' The function \code{L1centMDS()} is valid only for undirected and connected
#' graphs. Also, \code{L1centMDS()} only considers graphs with equal vertex
#' multiplicities.
#'
#' @param g An \code{igraph} graph object or a distance matrix.
#' * When \code{g} is an \code{igraph} object, the graph must be undirected and connected.
#' * When \code{g} is a matrix, it should be symmetric and represent a distance matrix.
#' All entries of the distance matrix must be finite.
#' @param edge_weight_transform An optional function to transform the edge weights
#'   when `g` is an `igraph` object and an edge weight attribute exists. This
#'   argument is ignored when `g` is a distance matrix.
#' @param tol A numerical tolerance. The gradient descent method terminates if
#'   the relative magnitude of the gradient falls below \code{tol} as in Kruskal
#'   (1964b). By default set to \ifelse{html}{\out{10<sup>-5</sup>}}{\eqn{10^{-5}}}.
#' @param maxiter A number of maximum iteration allowances for the gradient descent
#'   algorithm. By default set to 1000.
#' @param x An \code{L1centMDS} object, obtained as a result of the function
#'   \code{L1centMDS()}.
#' @param verbose A boolean.
#'  * `TRUE` (the default): for each iteration, prints (1) current number of
#'  iterations, (2) current stress, and (3) relative magnitude of the gradient
#'  to the console. At the end, the final message is printed to the console; total
#'  number of iterations and final stress.
#'  * `FALSE`: suppress message to the console.
#' @param zoom A numerical value on how much to zoom-in the plot. By default set
#'   to 1 (no zoom).
#' @param main Title of the plot. If set to \code{NULL} (the default), the title
#'   prints \dQuote{Target plot / Stress = X}.
#' @param show_edge A boolean.
#'  * `TRUE`: edges are shown in the target plot. When `x`, the `L1centMDS`
#'  object, is generated from a matrix object `g`, this argument is ignored
#'  since `g` does not carry information about the edge connections. That is, edges
#'  can only be drawn if the `L1centMDS` object is generated from an `igraph` object.
#'  * `FALSE` (the default): edges are not shown in the target plot.
#' @param ... Further arguments passed to or from other methods.
#'   * `plot()` method: Further graphical parameters supplied to the internal
#'   [base::plot()] (for points), [graphics::text()] (for labels), and
#'   [graphics::segments()] (for edges) functions. See [graphics::par()]
#'   document. To supply an argument to the first one, use the prefix
#'   \sQuote{\code{plot.}}, for the second one, \sQuote{\code{text.}}, and for
#'   the last one, \sQuote{\code{edge.}}. For instance, \code{plot.cex = 1} sets
#'   the size of the point, whereas \code{text.cex = 1} sets the size of the
#'   label.
#'   * `print()` method: This argument is ignored.
#'
#' @return \code{L1centMDS()} returns an object of class \code{L1centMDS}. It is a list
#'   consisting of three vectors:
#' \itemize{
#'  \item \sQuote{radius}: Radius of a point representing each vertex in the
#'  target plot's circular coordinate system, i.e., \eqn{-\log(L_1\text{
#'  centrality})} for each vertex.
#'  \item \sQuote{theta}: Angle (in radians) of a point representing each vertex
#'  in the target plot's circular coordinate system.
#'  \item \sQuote{stress}: Stress measure defined in Kruskal (1964a).
#' }
#'
#' \code{plot.L1centMDS()} draws a target plot. Four concentric circles denote
#' the 1st to 4th quartiles of the radius, and the values of the
#' \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}} centrality quartiles
#' are shown in red text. Note that red texts denote the
#' \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}} centrality quartiles,
#' \emph{not} radius quartiles.
#'
#' `print.L1centMDS()` prints number of iterations it took to fit a target plot.
#'
#' @export
#' @seealso [L1cent()] for
#'   \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}} centrality/prestige,
#'   [MASS::isoMDS()] and [stats::cmdscale()] for multidimensional scaling
#'   methods.
#' @examples
#' parameters <- L1centMDS(MCUmovie, verbose = FALSE)
#' plot(parameters)
#' @references S. Kang and H.-S. Oh. On a notion of graph centrality based on
#'   \ifelse{html}{\out{<i>L</i><sub>1</sub>}}{{\eqn{L_1}}} data depth.
#'   \emph{Journal of the American Statistical Association}, 121(553): 400--412, 2026.
#'
#'   J. B. Kruskal. Multidimensional scaling by optimizing goodness of fit to a
#'   nonmetric hypothesis. \emph{Psychometrika}, 29(1): 1--27, 1964a.
#'
#'   J. B. Kruskal. Nonmetric multidimensional scaling: a numerical method.
#'   \emph{Psychometrika}, 29(2): 115--129, 1964b.
L1centMDS <- function(g, tol, maxiter, verbose, edge_weight_transform) UseMethod("L1centMDS")

#' @name L1centMDS
#' @exportS3Method L1centMDS igraph
L1centMDS.igraph <- function(g, tol = 1e-5, maxiter = 1000, verbose = TRUE, edge_weight_transform = NULL){
  validate_igraph(g)

  new_weight <- edge_weight_transform0(g, edge_weight_transform)
  if(!is.null(new_weight)) igraph::E(g)$weight <- new_weight
  D <- igraph::distances(g)
  attr(D, "color.igraph") <- igraph::V(g)$color
  attr(D, "label.igraph") <- igraph::V(g)$label
  res <- L1centMDS.matrix(D, tol, maxiter, verbose)
  attr(res, "edgelist") <- igraph::as_edgelist(g)
  return(res)
}

#' @name L1centMDS
#' @exportS3Method L1centMDS matrix
L1centMDS.matrix <- function(g, tol = 1e-5, maxiter = 1000, verbose = TRUE, edge_weight_transform = NULL){
  eta <- rep(1,ncol(g))
  validate_matrix(g, eta)

  calls <- sys.calls()
  fnames <- sapply(calls, function(cl) as.character(cl[[1]]))
  # print(fnames)
  if(all(fnames[1:2] == c("L1centMDS", "L1centMDS.matrix")))
    message("DISTANCE matrix received")

  stepsize <- 0.2
  n <- ncol(g)
  label <- rownames(g)
  if(is.null(label)) label <- 1:nrow(g)
  dist.original.vec <- g[upper.tri(g)]
  cent <- L1cent(g, vertex_weight = eta)
  radius <- c(-log(cent))

  # initialize using classical MDS
  init <- stats::cmdscale(g)
  temp <- t(init)-init[which.min(radius),]
  # add small errors for stability
  params <- c(atan2(temp[2,],temp[1,])) + stats::rnorm(n,0,(pi/32)^2)

  # iterate until convergence
  iter.count <- 1
  stress <- Inf
  while(TRUE){
    # current distance matrix
    DIST <- as.matrix(stats::dist(cbind(radius*cos(params),radius*sin(params))))
    DIST.vec <- DIST[upper.tri(DIST)]

    # estimate HAT.DIST
    dist.original.group <- match(dist.original.vec, sort(unique(dist.original.vec)))
    ngroup <- length(unique(dist.original.group))
    group.mean.n <- t(stats::aggregate(DIST.vec,by=list(dist.original.group),function(s)c(mean(s),length(s)))[,2])

    group.mean.n[1,] <- .dhatestimate(group.mean.n[2,],group.mean.n[1,])

    HAT.DIST.vec <- group.mean.n[1,dist.original.group]
    HAT.DIST <- matrix(0,nrow=n,ncol=n)
    HAT.DIST[upper.tri(HAT.DIST)] <- HAT.DIST.vec
    HAT.DIST <- HAT.DIST + t(HAT.DIST)

    # current stress
    Tstar <- sum(DIST.vec^2)
    Sstar <- sum((DIST.vec-HAT.DIST.vec)^2)
    new.stress <- sqrt(Sstar/Tstar)

    # gradient descent method
    R <- matrix(rep(radius,n),ncol=n)
    P <- matrix(rep(params,n),ncol=n)
    gradient <- sqrt(Tstar/Sstar)/Tstar*rowSums(
      (DIST*(1-Sstar/Tstar) - HAT.DIST)*R*t(R)*
        sin(P - t(P))/
        sqrt(R^2 + t(R^2) - 2*R*t(R)*cos(P-t(P))),na.rm=TRUE ## remove 0/0
    )
    mag.gradient <- sqrt(sum(gradient^2)/sum(radius^2))
    if(iter.count > 1){
      # Kruskal (1964b, p.121) uses complicated formula for determining the
      # stepsize. The formula is determined heuristically, based on his
      # numerical experimentation. Since we are in a different setting where
      # points are constrained to lie on a concentric circle, we use the
      # following simple formula for updating the stepsize.
      stepsize <- stepsize * 0.95
    }
    params <- (params - stepsize * gradient/mag.gradient) %% (2*pi)

    if(verbose){
      cat("\rITER ",iter.count," / Stress: ", new.stress," / ",mag.gradient,"\t\t\r",sep="")
      utils::flush.console()
    }

    if(mag.gradient <= tol){
      stress <- new.stress
      if(verbose){
        cat("\rConverged / Iteration: ",iter.count," / Stress: ",stress,"             \n",sep="")
        utils::flush.console()
      }
      break;
    }
    if(iter.count >= maxiter){
      stress <- new.stress
      if(verbose){
        cat("\rStopped after maximum iteration: ",maxiter," / Stress: ",stress,"             \n",sep="")
        utils::flush.console()
      }
      break;
    }
    stress <- new.stress
    iter.count <- iter.count + 1
  }
  return(structure(list(radius=radius,theta=params,stress=stress),
                   class="L1centMDS",
                   label=label,
                   label.igraph=attr(g, "label.igraph"),
                   color.igraph=attr(g, "color.igraph"),
                   iteration = iter.count))
}


#' @name L1centMDS
#' @aliases plot.L1centMDS
#' @export
plot.L1centMDS <- function(x, zoom=1, main=NULL, show_edge=FALSE, ...){
  radius <- x$radius
  M.radius <- max(radius)
  theta <- x$theta

  args <- list(...)
  plot.args <- list()
  text.args <- list()
  edge.args <- list()
  if(!is.null(names(args))){
    plot.args <- args[startsWith(names(args), "plot.")]
    names(plot.args) <- substr(names(plot.args),6,nchar(names(plot.args)))
    text.args <- args[startsWith(names(args), "text.")]
    names(text.args) <- substr(names(text.args),6,nchar(names(text.args)))
    edge.args <- args[startsWith(names(args), "edge.")]
    names(edge.args) <- substr(names(edge.args),6,nchar(names(edge.args)))
  }
  if(is.null(plot.args$pch)) plot.args$pch <- 20
  if(is.null(plot.args$col)) plot.args$col <- attr(x, "color.igraph")
  if(is.null(text.args$cex)) text.args$cex <- 0.5
  if(is.null(text.args$labels)){
    text.args$labels <- if(is.null(attr(x, "label.igraph"))) attr(x, "label") else attr(x, "label.igraph")
  }

  withr::local_par(list(mar = c(1,1,4,1)+0.1))
  xx <- cos(seq(0,2*pi,length.out=200))
  yy <- sin(seq(0,2*pi,length.out=200))
  grid <- stats::quantile(radius,1)
  plot((grid)*xx,(grid)*yy,col="gray",type="l",
       asp=1,xlim=c(-M.radius,M.radius)/zoom,ylim=c(-M.radius,M.radius)/zoom,
       xlab="",ylab="",axes=FALSE,
       main=ifelse(is.null(main),paste0("Target plot / Stress = ",x$stress),main))
  graphics::text(grid*cos(3*pi/4),grid*sin(3*pi/4),round(exp(-grid),4),col="red",cex=0.5)
  for(grid in stats::quantile(radius,c(0.25,0.5,0.75))){
    graphics::lines((grid)*xx,(grid)*yy,col="gray")
    graphics::text(grid*cos(3*pi/4),grid*sin(3*pi/4),round(exp(-grid),4),col="red",cex=0.5)
  }
  if(show_edge){
    el <- attr(x, "edgelist")
    lb <- attr(x, "label")
    elm <- matrix(match(el, lb), ncol = 2)
    coordm <- cbind(radius * cos(theta), radius * sin(theta))
    x0 <- coordm[elm[,1], 1]
    x1 <- coordm[elm[,2], 1]
    y0 <- coordm[elm[,1], 2]
    y1 <- coordm[elm[,2], 2]
    do.call(graphics::segments, c(list(x0, y0, x1, y1),edge.args))
    # segments(x0, y0, x1, y1)
  }
  do.call(graphics::points, c(list(radius*cos(theta),radius*sin(theta)),plot.args))
  do.call(graphics::text, c(list(radius*cos(theta),radius*sin(theta)),text.args))
}

#' @name L1centMDS
#' @aliases print.L1centMDS
#'
#' @export
print.L1centMDS <- function(x, ...){
  cat("Target plot fitted after ", attr(x, "iteration"), " iterations\n", sep = "")
  return(invisible(x))
}
