#' as.phyloData
#' 
#' Convert a `phylo` object into a data frame of edge information and 
#' a vector of node information.
#' 
#' @param phy:  an S3 object of class `phylo` (`ape` package), which 
#'        must be a rooted tree.
#' @return an S3 object of class `phyloData`
#' 
#' @examples 
#' require(ape)
#' phy <- rtree(20)
#' as.phyloData(phy)
#' 
#' @export
as.phyloData <- function(phy, unscaled=FALSE) {
  if (!is.element('phylo', class(phy))) {
    stop("Error: as.phyloData requires an ape:phylo object as input.")
  }
  
  if (is.null(phy$node.label)) {
    phy$node.label <- paste("Node", 1:Nnode(phy), sep='')
  }
  if (is.null(phy$edge.length)) {
    unscaled <- TRUE
    phy$edge.length <- rep(NA, nrow(phy$edge))
  }
  
  # convert edge attributes into data frame
  edges <- data.frame(
    parent = phy$edge[,1],
    child = phy$edge[,2],
    length = phy$edge.length,
    isTip = (phy$edge[,2] <= Ntip(phy))
  )
  
  # convert node attributes to data frame
  nodes <- data.frame(
    label = c(phy$tip.label, phy$node.label)
  )
  
  # calculate number of tips per node
  #st <- subtrees(phy)
  nodes$n.tips <- c(rep(0, Ntip(phy)), count.tips(phy)) #sapply(st, Ntip))

  # carry over any non-default attributes
  # we assume these are ordered correctly!
  default.keys <- c('tip.label', 'node.label', 'Nnode', 'edge', 'edge.length')
  for (key in names(phy)) {
    if (!is.element(key, default.keys)) {
      vec <- phy[[key]]
      if (length(vec) == nrow(edges)) {
        edges[key] <- vec
      }
      else if (length(vec) == nrow(nodes)) {
        # NOTE nodes are unchanged in order (numbering)
        nodes[key] <- vec
      }
      else {
        warning("as.phyloData ignoring attribute ", key, 
                " of length ", length(vec))
      }
    }
  }

  # total branch length from root to node
  if (unscaled) {
    nodes$x <- Ntip(phy)-node.depth(phy)
  } else {
    nodes$x <- node.depth.edgelength(phy)
  }
  
  # return to original ordering (we don't have to change nodes)
  #index <- match(phy$edge[,2], edges$child)
  
  obj <- list(edges=edges, nodes=nodes)
  class(obj) <- 'phyloData'
  obj
}


#' count.tips
#' 
#' Calculate the number of tips for every internal node in the tree.
#' 
#' @param phy: an S3 object of class `phylo` (`ape`` package)
#' @return numeric vector of length Nnode(phy), sorted in preorder
#' @export
count.tips <- function(phy) {
  # check inputs
  if (!is.element('phylo', class(phy))) {
    stop("`phy` must be an object of class ape::phylo")
  }
  if(!all(table(phy$edge[,2])==1)) {
    stop("Each node index should appear only once in phy$edge[,2]")
  }
  
  check.idx <- sapply(1:nrow(phy$edge), function(i) {
    cidx <- phy$edge[i,2]
    if (is.element(cidx, phy$edge[,1])) {
      j <- min(which(phy$edge[,1]==cidx), na.rm=T)
      return(i < j)
    }
    return(NA)
    })
  if (!all(check.idx, na.rm=TRUE)) {
    stop("All nodes must appear as child (phy$edge[,2]) before parent (phy$edge[,1])")
  }
         
  ntips <- rep(0, Ntip(phy) + Nnode(phy))
  for (i in seq(nrow(phy$edge), 1)) {
    parent <- phy$edge[i, 1]
    child <- phy$edge[i, 2]
    if (child <= Ntip(phy)) {
      ntips[parent] <- ntips[parent] + 1  # child is a tip
    } else {
      ntips[parent] <- ntips[parent] + ntips[child]
    }
  }
  return(ntips[(Ntip(phy)+1):length(ntips)])
}


#' print.phyloData
#' 
#' Generic function to display object.
#' @param obj:  S3 object of class `phyloData`
#' @export
print.phyloData <- function(obj) {
  n <- sum(obj$edges$isTip)
  cat("Phylogenetic data with", n, "tips and",
      nrow(obj$nodes)-n, "internal nodes.\n")
  cat("nodes:\n")
  cat(str(head(obj$nodes)))
  cat("edges:\n")
  cat(str(head(obj$edges)))
}


#' tree.layout
#' 
#' Generate the coordinates for the nodes and edges of a 
#' phylogenetic tree (ape:phylo object) using one of several
#' layout algorithms.  Used before calling the generic plot
#' functions (plot, points).
#' 
#' @param phy:  an S3 object of class `phylo``
#' @param type:  what type of layout algorithm to employ:
#' \itemize {
#'   \item{"r"}{Rectangular layout}
#'   \item{"s"}{Slanted (triangular) layout}
#'   \item{"u"}{Unrooted (equal-angle) layout}
#'   \item{"o"}{Circular (radial) layout}
#' }
#' @param unscaled:  if TRUE, return a cladogram layout
#' 
#' @return S3 object of class `phyloLayout`
#' 
#' @examples 
#' require(ape)
#' phy <- rcoal(20)
#' lay <- tree.layout(phy, 'r')
#' head(lay$edges)
#' 
#' @export
tree.layout <- function(phy, type='r', unscaled=FALSE) {
  if ( !is.element('phylo', class(phy)) ) {
    stop("tree.layout() requires `phy` to be an ape:phylo object.")
  }

  if (type=='r') {
    # rectangular
    .layout.rect(phy, unscaled, slanted=FALSE)
  }
  else if (type=='s') {
    # slanted
    .layout.rect(phy, unscaled, slanted=TRUE)
  }
  else if (type=='u') {
    # unrooted
    .layout.equalangle(phy, unscaled=unscaled)
  }
  else if (type == 'o') {
    # circular (radial)
    .layout.radial(phy, unscaled=unscaled)
  }
  else {
    stop("Unrecognized layout type '", type, "'")
  }
}


#' print.phyloLayout
#' 
#' Generic function to display object.
#' @param obj:  S3 object of class `phyloLayout`
#' @export
print.phyloLayout <- function(obj) {
  n <- sum(obj$edges$isTip)
  cat("Phylogenetic layout using a", obj$layout, "algorithm\n",
      "with", n, "tips and", nrow(obj$nodes)-n, "internal nodes.\n\n")
  cat(paste0("$nodes (", nrow(obj$nodes)), "nodes with", ncol(obj$nodes), "attributes):\n")
  print(head(obj$nodes))
  cat("\n")
  cat(paste0("$edges (", nrow(obj$edges)), "edges with", ncol(obj$edges), "attributes):\n")
  print(head(obj$edges))
  cat("\n")
}


.get.root <- function(edges) {
  idx <- which(!is.element(edges$parent, edges$child))
  root <- unique(edges$parent[idx])
  if (length(root) == 0) {
    stop("Failed to find root in .get.root")
  }
  if (length(root) > 1) {
    stop("Found multiple roots in .get.root")
  }
  root
}

.reorder.nodes <- function(edges, node, order, result=c()) {
  if (order=='preorder') {
    result <- c(result, node)  # parent before children
  }
  
  children <- edges$child[edges$parent == node]
  for (child in children) {
    result <- .reorder.nodes(edges, child, order, result)
  }
  if (order=='postorder') {
    result <- c(result, node)  # children before parent
  }
  return(result)
}



#' .layout.rect
#' 
#' Algorithm to generate a "rectangular" (left to right) layout of a 
#' phylogenetic tree, with the root at the left and tips on the right.
#' 
#' @keywords internal
.layout.rect <- function(phy, unscaled=FALSE, slanted=FALSE) {
  # convert phylo object to data frames
  pd <- as.phyloData(phy, unscaled)
  
  tips <- pd$edges$child[pd$edges$isTip]
  pd$nodes$y <- rep(NA, nrow(pd$nodes))
  pd$nodes$y[tips] <- 1:Ntip(phy)
  
  # assign vertical positions to internal nodes
  root <- .get.root(pd$edges)
  for (i in .reorder.nodes(pd$edges, root, order='postorder')) {
    if (i > Ntip(phy)) {
      children <- pd$edges$child[pd$edges$parent==i]
      pd$nodes$y[i] <- mean(pd$nodes$y[children])
    }
  }

  # map node coordinates to edges
  pd$edges$x0 <- pd$nodes$x[pd$edges$parent]
  pd$edges$x1 <- pd$nodes$x[pd$edges$child]
  if (slanted) {
    pd$edges$y0 <- pd$nodes$y[pd$edges$parent]  
  } else {
    pd$edges$y0 <- pd$nodes$y[pd$edges$child]
  }
  pd$edges$y1 <- pd$nodes$y[pd$edges$child]
  
  pd$layout <- ifelse(slanted, 'slanted', 'rectangular')
  pd$postorder <- postorder(phy)
  
  class(pd) <- c('phyloLayout', 'phyloData')
  pd
}



#' layout.radial
#' 
#' Generate the coordinates for a radial layout of a phylogenetic tree
#' within a circular region.
#' 
#' @param phy:  an S3 object of class `phylo`
#' 
#' @return S3 object of class `phyloLayout`
#' @keywords internal
.layout.radial <- function(phy, unscaled) {
  # convert phylo object to data frames
  pd <- as.phyloData(phy, unscaled)
  pd$nodes$r <- pd$nodes$x  # reassign depth to radius
  
  tips <- pd$edges$child[pd$edges$isTip]
  pd$nodes$angle <- rep(NA, nrow(pd$nodes))
  pd$nodes$angle[tips] <- (1:Ntip(phy)) / Ntip(phy) * 2 * pi
  
  # assign angles to internal nodes
  root <- .get.root(pd$edges)
  for (i in .reorder.nodes(pd$edges, root, order='postorder')) {
    if (i > Ntip(phy)) {
      children <- pd$edges$child[pd$edges$parent==i]
      pd$nodes$angle[i] <- mean(pd$nodes$angle[children])
    }
  }
  
  # calculate x,y from polar coordinates
  pd$nodes$x <- pd$nodes$r * cos(pd$nodes$angle)
  pd$nodes$y <- pd$nodes$r * sin(pd$nodes$angle)
  
  # map node coordinates to edges
  pd$edges$x1 <- pd$nodes$x[pd$edges$child]
  pd$edges$y1 <- pd$nodes$y[pd$edges$child]
  
  pd$edges$x0 <- pd$nodes$r[pd$edges$parent] * cos(pd$nodes$angle[pd$edges$child])
  pd$edges$y0 <- pd$nodes$r[pd$edges$parent] * sin(pd$nodes$angle[pd$edges$child])
  
  pd$layout <- 'radial'
  class(pd) <- c('phyloLayout', 'phyloData')
  pd
}


#' layout.equalangle
#' 
#'  Genearte the coordinates for an unrooted layout of a phylogenetic
#'  tree using Joe Felsenstein's equal-angle algorithm.
#'  
#'  @param phy:  an S3 object of class `phylo`
#'  
#'  @return S3 object of class `phyloLayout`
#'  @keywords internal
.layout.equalangle <- function(phy, unscaled) {
  if (is.rooted(phy)) {
    phy <- unroot(phy)
    # unrooting destroys internal node labels
    phy$node.label <- paste("Node", 1:Nnode(phy), sep='')
  }
  pd <- as.phyloData(phy, unscaled=unscaled)
  
  # allocate new node attributes
  pd$nodes$start <- NA
  pd$nodes$end <- NA
  pd$nodes$angle <- NA
  pd$nodes$r <- pd$nodes$x  # depth becomes radius
  pd$nodes$x <- NA
  pd$nodes$y <- NA
  
  # initialize at root
  root <- unique(pd$edges$parent[which(
    !is.element(pd$edges$parent, pd$edges$child)
    )])
  if (length(root) != 1) {
    stop("Failed to locate root in .layout.equalangle")
  }

  pd$nodes$start[root] <- 0.
  pd$nodes$end[root] <- 2*pi
  pd$nodes$angle[root] <- 0.  # arbitrary
  pd$nodes$x = 0;  # map to origin
  pd$nodes$y = 0;
  
  pd <- .recursive.equalAngles(root, pd)
  
  # map node coordinates to edges
  pd$edges$x0 <- pd$nodes$x[pd$edges$parent]
  pd$edges$x1 <- pd$nodes$x[pd$edges$child]
  pd$edges$y0 <- pd$nodes$y[pd$edges$parent]  
  pd$edges$y1 <- pd$nodes$y[pd$edges$child]  
  
  class(pd) <- c('phyloLayout', 'phyloData')
  pd$layout <- 'equal.angle'
  
  pd
}


#' .recursive.equalAngles
#' 
#' A recursive function for calculating Felsenstein's equal-angles algorithm
#' for unrooted tree layouts.
#' 
#' @param index:  row number of current node in data frame
#' @param pd:  S3 object of class `phyloData``
#' 
#' @return `phyloData` object with updated `nodes` data frame
#' 
#' @keywords internal
.recursive.equalAngles <- function(index, pd) {
  edges <- pd$edges
  node <- pd$nodes[index, ]
  
  last.start <- node$start
  children <- edges$child[edges$parent==index]
  for (i in children) {
    child <- pd$nodes[i, ]
    
    # assign proportion of arc to this child
    arc <- (node$end - node$start) * max(child$n.tips, 1)/node$n.tips
    child$start <- last.start
    child$end <- child$start + arc
    
    # bisect the arc
    child$angle <- child$start + (child$end - child$start)/2.
    last.start <- child$end
    
    # map to x,y coordinates
    child$x <- node$x + (child$r-node$r) * sin(child$angle)
    child$y <- node$y + (child$r-node$r) * cos(child$angle)
    
    # save to data frame
    pd$nodes[i, ] <- child
    
    # climb up tree
    pd <- .recursive.equalAngles(i, pd)
  }
  
  return(pd)
}


#' plot.phyloLayout
#' 
#' Generic plot function for phylogenetic trees with layouts generated
#' using ggfree.  The coordinate system of the plot depends on the layout
#' algorithm:
#' \itemize {
#'   \item Rectangular/slanted layout: The x-axis is scaled to the height
#'   of the tree (from the root to the furthest tip).  The y-axis is scaled 
#'   to the number of tips (1:Ntip(phy)).
#'   \item Circular (radial) layout:  
#' }
#' Graphical parameters such as `col` or `lwd` can take either a single value 
#' or a vector to specify custom values for each edge.  The ordering should
#' be the same as the original tree (phylo) object, which defaults to 
#' preorder traversal.
#' 
#' @param obj:  S3 object of class `phyloLayout`
#' @param type:  if 'n', then line segments are not drawn
#' @param col:  colour for line segments.  Defaults to 'grey50'.
#' @param lwd:  stroke width for line segments.  Defaults to 2.
#' @param label:  Specifies whether nodes are labeled with text.
#'   \itemize{
#'     \item{'n'}{No node labels.}
#'     \item{'t'}{Tip labels only (default).}
#'     \item{'i'}{Internal node labels only.}
#'     \item{'b'}{Both tip and internal node labels.}
#'   }
#' @param cex.lab:  Character expansion factor for node labels (default 0.8).
#' @param mar:  (optional) vector of margin widths (graphical parameter).
#' @param ...:  additional graphical parameters to pass to `lines` and `label`
#' 
#' @examples 
#' # generate random coalescent tree
#' set.seed(1999); phy <- rcoal(50)
#' 
#' # rectangular tree
#' plot(tree.layout(phy, type='r'), mar=c(3,0,0,1))
#' axis(side=1)
#' 
#' # unscaled slanted tree with colors
#' pal <- colorRampPalette(c('cadetblue', 'brown'))
#' plot(tree.layout(phy, type='s', unscaled=TRUE), col=pal(5))
#' 
#' # unrooted tree
#' plot(tree.layout(phy, type='u'))
#' 
#' # circular (radial) tree
#' plot(tree.layout(phy, type='o'), label='t')
#' 
#' @export
plot.phyloLayout <- function(obj, type='l', col='grey50', lwd=2, label='t', cex.lab=0.8, 
                             mar=NA, xlim=NA, add=FALSE, offset=0, ...) {
  # check inputs
  if (!is.element('phyloLayout', class(obj))) {
    stop("Argument `obj` must be S3 object of class `phyloData`")
  }
  if (is.null(obj$nodes$y)) {
    stop("You have to apply one of the layout functions to the `phyloData` ",
         "object before plotting! e.g., `layout.rect()`")
  }
  
  # prepare the plot region
  if (!add) {
    if (is.element(obj$layout, c('slanted', 'rectangular'))) {
      if (any(is.na(mar))) {
        par(mar=c(2,1,1,5))  # default margins
      } else {
        par(mar=mar)
      }
      if (any(is.na(xlim))) {
        xlim=c(0, max(obj$nodes$x))  # default
      }
      plot(NA, xlim=xlim, ylim=c(0, max(obj$nodes$y)+1),
           main=NA, xlab=NA, ylab=NA, xaxt='n', yaxt='n', bty='n')
    }
    else if (obj$layout=='radial' | obj$layout == 'equal.angle') {
      if (any(is.na(mar))) {
        # default for radial layout
        par(mar=rep(2, 4))
      } else {
        par(mar=mar)
      }
      if (any(is.na(xlim))) {
        xlim=range(obj$nodes$x)  # default
      }
      plot(NA, xlim=xlim, ylim=range(obj$nodes$y),
           main=NA, xlab=NA, ylab=NA, xaxt='n', yaxt='n', bty='n')
    }
  }
  
  if (type != 'n') {
    # draw line segments
    suppressWarnings({
      lines(obj, col=col, lwd=lwd, ...)  
    })
  }
  
  if (label != 'n') {
    # draw node labels
    suppressWarnings({
      text.phyloLayout(obj, label=label, cex.lab=cex.lab, offset=offset, ...)
    })
  }
}


#' lines.phyloLayout
#' 
#' Generic S3 method adapted for drawing the line segments that 
#' comprise a phylogenetic tree.
#' 
#' @param obj:  an S3 object of class `phyloLayout`
#' @param col:  if a colour vector is specified, `lines` will apply
#'        the colors in the same order as edges in the data frame 
#'        in `obj`.  If the vector is too short, it will recycle colors.
#' @param shade:  if TRUE (default), vertical edges will be coloured 
#'        by the average of descendant horizonatal edges.  If FALSE,
#'        vertical edges are uniformly coloured grey.
#' @param ...:  additional graphical parameters to pass to `segments`
#'        and `draw.arc`
#' 
#' @export
lines.phyloLayout <- function(obj, col='grey50', shade=TRUE, ...) {
  if (length(col) < nrow(obj$edges)) {
    # recycle colours if necessary
    col <- rep(col, length.out=nrow(obj$edges))
  }
  
  # this applies for all layouts
  segments(x0=obj$edges$x0, y0=obj$edges$y0, 
           x1=obj$edges$x1, y1=obj$edges$y1, col=col, ...)
  
  # group edges by parent node
  temp <- split(obj$edges, obj$edges$parent)
  
  # draw vertical edges for rect or radial layouts
  for (e in temp) {
    
    # determine edge color
    if (shade) {
      pal <- col[as.integer(row.names(e))]
      this.col <- blend.colors(pal)
    }
    else {
      this.col <- 'grey50'
    }
    
    if (obj$layout == 'rectangular') {
      
      x0 <- e[1,]$x0  # should be the same for all entries
      y0 <- min(e$y0)
      y1 <- max(e$y0)

      segments(x0=x0, y0=y0, x1=x0, y1=y1, col=this.col, ...)
    }
    else if (obj$layout == 'radial') {
      nodes <- obj$nodes[e$child,]
      parent <- obj$nodes[unique(e$parent),]
      start <- min(nodes$angle)
      end <- max(nodes$angle)
        
      draw.arc(x=0, y=0, theta0=start, theta1=end, r0=parent$r, 
               col=this.col, ...)
    }
  }
}


#' text.phyloLayout
#' 
#' Draw tip and/or internal node labels on the phylogenetic tree.
#' 
#' @param obj:  an S3 object of class `phyloLayout`
#' @param label:  Specifies which set of nodes are labeled with text.
#'   \itemize{
#'     \item{'t'}{Tip labels only (default).}
#'     \item{'i'}{Internal node labels only.}
#'     \item{'b'}{Both tip and internal node labels.}
#'   }
#' @param align:  if TRUE, all tip labels are drawn at maximum value
#' @param cex.lab:  character expansion factor for text
#' @param offset:  float, additional spacing between tree tip and label; 
#'                 defaults to 0.
#' @param col:  vector of colour values, should be in same order as labels; 
#'              if a single value, is applied to all labels; defaults to 'black'
#' @param ...:  additional graphical parameters passed to `text`
#' 
#' @export
text.phyloLayout <- function(obj, label='t', align=FALSE, cex.lab=1, offset=0, 
                             col='black', ...) {
  
  # filter node data frame
  tips <- obj$nodes[obj$nodes$n.tips==0, ]
  internals <- obj$nodes[obj$nodes$n.tips>0, ]
  
  if (length(col) == 1) {
    col <- rep(col, nrow(obj$nodes))
  } 
  else if (length(col) < nrow(obj$nodes)) {
    warning("vector `col` is shorter than number of labels, recycling")
    col <- rep(col, length.out=nrow(obj$nodes))
  }
  col.tips <- col[1:nrow(tips)]
  col.ints <- col[(nrow(tips)+1):nrow(obj$nodes)]
  
  # allow drawing into margins of plot device
  par(xpd=NA)
  
  if (obj$layout=='rectangular' | obj$layout=='slanted') {
    if (is.element(label, c('t', 'b'))) {
      # draw tip labels
      x <- tips$x + offset
      if (align) { x <- max(tips$x) + offset }
      text(x=x, y=tips$y, labels=paste0(' ', tips$label), 
           adj=0, cex=cex.lab, col=col.tips, ...)
    }
    if (is.element(label, c('i', 'b'))) {
      # draw internal labels
      text(x=internals$x, y=internals$y, labels=paste0(' ', internals$label), 
           adj=0, cex=cex.lab, col=col.ints, ...)
    }
  }
  else if (obj$layout == 'radial' | obj$layout == 'equal.angle') {
    if (is.element(label, c('t', 'b'))) {
      # srt does not work for vectors
      for (i in 1:nrow(tips)) {
        tip <- tips[i, ]
        
        # equal angle layout draws zero-angle straight up
        if (obj$layout == 'equal.angle') tip$angle <- pi/2-tip$angle
        tip <- .rotate.label(tip, offset)
        
        text(x=tip$x, y=tip$y, labels=tip$label, 
             srt=tip$angle/pi*180, adj=as.integer(tip$rotated), 
             cex=cex.lab, col=col.tips[i], ...)
      }
    }
    if (is.element(label, c('i', 'b'))) {
      # draw internal labels
      for (i in 1:nrow(internals)) {
        node <- internals[i, ]
        
        if (obj$layout == 'equal.angle') node$angle <- pi/2-node$angle
        node <- .rotate.label(node, offset)
        
        text(x=node$x, y=node$y, labels=node$label, 
             srt=node$angle/pi*180, adj=as.integer(node$rotated), 
             cex=cex.lab, col=col.ints[i], ...) 
      }
    }
  }

  # revert to default trimming at margins    
  par(xpd=FALSE)
}


#' .rotate.label
#' Adjust the string rotation of node labels for unrooted
#' or radial trees so they are not upside-down.
#' 
#' @param node:  named vector, a row from the nodes data frame of a 
#'        `phyloLayout` object.
#' @param offset:  amount to push label outward from origin
#' 
#' @return  a named vector with updated `x`, `y`, `angle` and `label` values
#' 
#' @keywords internal
.rotate.label <- function(node, offset) {
  # slide label outward
  node$x <- node$x + offset*cos(node$angle)
  node$y <- node$y + offset*sin(node$angle)
  
  h <- node$angle %% (2*pi)
  if (h>0.5*pi && h<(1.5*pi)) {
    # invert the label
    node$angle <- node$angle+pi
    # pad the label on the right
    node$label <- paste0(node$label, ' ')
    node$rotated <- TRUE
  }
  else {
    # pad the label on the left
    node$label <- paste0(' ', node$label)
    node$rotated <- FALSE
  }
  node
}


#' points.phyloLayout
#' 
#' A generic function to add points to the plot.  This is 
#' customized for phyloLayout S3 objects that provide the x-
#' and y- coordinates for tips and internal nodes.
#' 
#' @param obj:  S3 object of class `phyloLayout`
#' @param type:  specify 'b' to plot both tips and internal nodes (default);
#'               't' plots only tips, and 'i' plots only internal nodes.
#'               NOTE: any additional parameters (`...`) will be interpreted 
#'               accordingly!
#' @param offset:  float, amount to draw points away from nodes.  This is 
#'                 generally most useful for tips (type='t').  Suported for all
#'                 layout algorithms.
#' @param ...:  additional graphical parameters to pass to points()
#'
#' @examples
#' require(ggfree)
#' set.seed(1)
#' phy <- rtree(20)
#' L <- tree.layout(phy, 'u')
#' plot(L, label='n')
#' points(L, type='t', pch=21, bg=hcl.colors(20), cex=1.5, offset=0.2, xpd=NA)
#' 
#' @export
points.phyloLayout <- function(obj, type='b', offset=0, ...) {
  x <- obj$nodes$x
  y <- obj$nodes$y
  
  if (offset != 0) {
    if (obj$layout == 'rectangular' | obj$layout == 'slanted') {
      x <- x + offset
    }
    else if (obj$layout == 'radial') {
      x <- (obj$nodes$r + offset) * cos(obj$nodes$angle)
      y <- (obj$nodes$r + offset) * sin(obj$nodes$angle)
    }
    else if (obj$layout == 'equal.angle') {
      x <- x + offset * sin(obj$nodes$angle)
      y <- y + offset * cos(obj$nodes$angle)
    }
    else {
      stop("Unrecognized layout", obj$layout, "in points.phyloLayout()")
    }
  }
  
  if (type=='b') {
    points(x=x, y=y, ...)    
  }
  else {
    is.tip <- (obj$nodes$n.tips == 0)
    if (type=='t') {
      points(x=x[is.tip], y=y[is.tip], ...)
    }
    else if (type == 'i') {
      points(x=x[!is.tip], y=y[!is.tip], ...)
    }
    else {
      stop("Unrecognized type", type , "in points.phyloLayout()")
    }
  } 
}



#' add.scalebar
#' Add a scale bar to the plot.
#' @param obj:  `phyloLayout` S3 object generated by `tree.layout`
#' @param len:  length of scale bar to draw (defaults to 1)
#' @param x0:  (optional) override default horizontal placement of bar
#' @param y0:  (optional) override default vertical placement of bar.
#'        Note both x and y need to be specified to override the default.
#' @param dy:  (optional) override default spacing between bar and label.
#' @param cex.lab:  scale factor for label
#' @param ...:  additional graphical parameters to pass to `segments`, 
#'        such as `cex`, `col` and `lwd`.
#' 
#' @examples 
#' require(ape)
#' phy <- rcoal(30)
#' Y <- tree.layout(phy, type='r')
#' plot(Y)
#' add.scalebar(Y, len=1)
#' @export
add.scalebar <- function(obj, len=1, x0=NA, y0=NA, dy=NA, lwd=2, cex.lab=1, ...) {
  if (is.na(x0)) {
    mid.pt <- mean(range(obj$nodes$x))
    x0 <- mid.pt - len/2  # use default location
    x1 <- mid.pt + len/2
  } else {
    x1 <- x0+len
  }

  if (is.na(dy)) {
    dy <- 0.01*diff(range(obj$nodes$y))
  }
  
  if (is.na(y0)) {
    y0 <- min(obj$nodes$y) - dy
  }
  suppressWarnings({
    segments(x0, y0, x1, lwd=lwd, xpd=NA, ...)
    text(x=mean(c(x0, x1)), y=y0-dy, label=len, cex=cex.lab, xpd=NA, ...)
  })
}


#' unroot
#' 
#' If the `phylo` object contains non-default attributes for nodes
#' or edges, then unrooting breaks the alignment between these 
#' attributes and the tree. 
#' 
#' @export
unroot <- function(obj) {
  # remove node and edge attributes associated with the root node
  if (is.rooted(obj)) {
    n.nodes <- Ntip(obj) + Nnode(obj)
    n.edges <- nrow(obj$edge)
    
    root <- unique(obj$edge[,1][which(
      !is.element(obj$edge[,1], obj$edge[,2])
      )])
    if (length(root) > 1) {
      stop("Error: found multiple root nodes in tree")
    }
    root.edges <- which(obj$edge[,1] == root)
    
    default.keys <- c('tip.label', 'node.label', 'Nnode', 'edge', 'edge.length')
    for (key in names(obj)) {
      if (!is.element(key, default.keys)) {
        val <- obj[[key]]
        if (length(val) == n.nodes) {
          # filter node attribute associated with root
          obj[[key]] <- val[-root]
        }
        else if (length(val) == n.edges) {
          # filter edge attributes associated with root
          obj[[key]] <- val(-root.edges)
        }
      }
    }
  }
  
  unroot.phylo(obj)
}


#' draw.guidelines
#' 
#' Draws segments from the tips of the tree out to the furthest
#' tip label.  This should only be used in conjunction with 
#' text.phyloLayout(align=TRUE), or else the segments will be drawn
#' over the tip labels.
#' 
#' @param obj:  S3 object of class `phyloLayout`
#' @param lty:  line style, defaults to 3 (dotted)
#' @param ...:  other graphical parameters to pass to `segments`,
#'        such as `col` or `lwd`
#'        
#' @export
draw.guidelines <- function(obj, lty=3, ...) {
  # right ends of terminal edges
  x0 <- obj$edges$x1[obj$edges$isTip]
  y0 <- obj$edges$y0[obj$edges$isTip]
  
  # map colours from edges to nodes
  index <- obj$edges$child[obj$edges$isTip]
  suppressWarnings(segments(x0=x0, x1=max(x0), y0=y0, lty=lty, ...))
}


#' image
#' 
#' Generic function for drawing a grid of coloured or grey-scale 
#' rectangles corresponding to values in a matrix `z`.  Rows in 
#' the matrix are assumed to correspond to tips of the tree.
#' 
#' @param obj:  an S3 object of class `phyloLayout`
#' @param z:  matrix, data to annotate tips in order of *nodes*; this will be 
#'            recast as a factor, and then an integer vector.  If your input 
#'            `z` is a numeric (continuous-valued) vector, then you should run 
#'            `cut(z)` to discretize the distribution.
#' @param xlim:  limits (x1, x2) of grid relative to current plot device.
#'               For a radial tree layout, these correspond to the inner and 
#'               outer radii.  You may need to use trial-and-error to find 
#'               a good set of boundaries.  Note this function will call 
#'               `xpd=NA` to permit drawing in margins.
#'               Defaults to `max(x)+0.01*range(x)` to `max(x)+0.06*range(x)`
#' @param col:  a vector of colours that maps to factor levels in `z`
#' @param border:  colour for border of rectangles in grid
#' @param xaxt:  if 'n', suppress drawing of axis and labels
#' @param lty:  line type for rect, defaults to par('lty')
#' @param lwd:  line width for rect, defaults to par('lwd')
#' 
#' @export
image.phyloLayout <- function(obj, z, xlim=NA, col=NA, border='white', xaxt='y', 
                              lty=NA, lwd=NA, ...) {
  # resolve default arguments
  if (all(is.na(lty))) {
    lty <- par('lty')
  }
  if (all(is.na(lwd))) {
    lwd <- par('lwd')
  }
  
  # recode contents of `z` as integer-valued matrix
  if (is.matrix(z)) {
    z <- apply(z, 2, function(x) as.integer(as.factor(x)))  
  } else {
    z <- sapply(z, function(x) as.integer(as.factor(x)))
  }
  
  # use default colors if not specified by user
  if (any(is.na(col))) {
    col <- colorRampPalette(c('firebrick', 'dodgerblue'))(max(z, na.rm=T))
  }
  
  #tips <- obj$edges[obj$edges$isTip, ]
  tips <- obj$nodes[obj$nodes$n.tips==0, ]
  
  # check that z has the expected dimensions
  if (nrow(z) != nrow(tips)) {
    stop("Number of rows in matrix `z` does not match number of tips ",
         "in `phyloLayout` object: ", nrow(z), "!=", nrow(tips))
  }
  
  # draw the image
  par(xpd=NA)
  
  if (obj$layout == 'rectangular' | obj$layout == 'slanted') {
    y <- tips$y
    
    if (any(is.na(xlim))) {
      # guess at decent default limits
      xrange <- range(obj$nodes$x)
      xlim <- c(max(xrange) + 0.01*diff(xrange),
                max(xrange) + 0.06*diff(xrange))
    }
    x <- seq(xlim[1], xlim[2], length.out=ncol(z)+1)
    dx <- (x[2]-x[1])/2
    
    for (j in 1:ncol(z)) {
      for (i in 1:nrow(z)) {
        val <- z[i,j]
        if (is.na(val)) next
        rect(xleft = x[j], xright = x[j+1], 
             ybottom = y[i]-0.5, ytop = y[i]+0.5,
             border = border, col = col[val], lwd=lwd, lty=lty)
      }
    }
    
    # draw axis (override masking of labels)
    if (xaxt != 'n') {
      odds <- seq(1, ncol(z), 2)
      evens <- odds+1
      suppressWarnings({
        axis(side=1, at=x[1:ncol(z)]+dx, labels=NA, ...)
        axis(side=1, at=x[odds]+dx, labels=names(geno)[odds], lwd=0, ...)
        axis(side=1, at=x[evens]+dx, labels=names(geno)[evens], lwd=0, ...)
      })
    }
  }
  else if (obj$layout == 'radial') {
    angles <- tips$angle
    d.theta <- pi/nrow(tips)
    
    if (any(is.na(xlim))) {
      # guess at decent default limits
      r.range <- range(obj$nodes$r)
      xlim <- c(max(r.range) + 0.01*diff(r.range),
                max(r.range) + 0.06*diff(r.range))
    }
    r <- seq(xlim[1], xlim[2], length.out=ncol(z)+1)
    dr <- (r[2]-r[1])/2
    
    for (j in 1:ncol(z)) {
      for (i in 1:nrow(z)) {
        val <- z[i,j]
        if (is.na(val)) next
        draw.arc(x = 0, y = 0, theta0 = angles[i]-d.theta, 
                 theta1 = angles[i]+d.theta, r0 = r[j], r1 = r[j+1],
                 col = col[val], border = border, lty=lty, lwd=lwd)
      }
    }
  }
  par(xpd=FALSE)
}


#' Traverse tree from given node (tip) to root, return 
#' vector of indices.
#' @param node:  integer, index of child node
#' @param edges:  data frame, from phyloLayout
#' @param result:  integer, vector of parent nodes
#' @param parents:  integer, vector of parent nodes that have 
#'                  already been visited
#' @keywords internal
.climb.up.tree <- function(node, edges, result=c(), parents=c()) {
  if (is.element(node, edges$child)) {
    parent <- edges$parent[edges$child==node]
    if (is.element(parent, parents)) {
      return(result)
    }
    result <- c(result, parent)
    result <- .climb.up.tree(parent, edges, result, parents)
  }
  return(result)
}


#' draw.branch
#'
#' Locate the common ancestor given a vector of tip labels and 
#' draw the corresponding branch or subset tree (sst=TRUE) for a 
#' given tree layout.
#' @param obj:  an S3 object of class `phyloLayout`
#' @param tips:  a character vector of tip labels
#' @param col:  color for drawing branch, defaults to red
#' @param ...:  additional arguments passed to `segments`
#' 
#' @export
draw.branch <- function(obj, tips, col='red', sst=FALSE, ...) {
  unmatched <- !is.element(tips, obj$nodes$label)
  if (any(unmatched)) {
    stop("Error: not all tip labels found in tree layout:")
    cat(tips[!unmatched])
  }
  
  # find most recent common ancestor
  idx <- sapply(tips, function(l) which(obj$nodes$label==l))
  traj <- lapply(idx, function(i) .climb.up.tree(i, obj$edges))
  
  if (sst) {  # subset tree
    # draw tips
    for (node in idx) {
      e <- obj$edges[obj$edges$child==node, ]
      segments(e$x0, e$y0, e$x1, e$y1, col, ...)      
    }
    # draw internal branches
    common <- unique(unlist(traj))
    for (node in common) {
      e <- obj$edges[obj$edges$child==node, ]
      segments(e$x0, e$y0, e$x1, e$y1, col, ...)
    }
  } else {
    # draw only the branch associated with the MRCA
    common <- Reduce(intersect, traj)
    if (length(common) < 1) {
      stop("Error: Failed to locate common ancestor of tips ", tips)
    }
    mrca <- common[1]
    
    if (!is.element(mrca, obj$edges$child)) {
      warning("draw.branch() selected root branch, no action taken.")
    } else {
      e <- obj$edges[obj$edges$child==mrca, ]
      segments(e$x0, e$y0, e$x1, e$y1, col, ...)
    }    
  }

}


#' find.clade
#' 
#' Locate common ancestor in the tree given a set of tip labels
#' and return all edges in subtree (clade) of all descendants.
#' 
#' @param obj:  an S3 object of class `phyloLayout`
#' @param tips:  a character vector of tip labels
#' @param is.mono:  colour monophyletic clade (from common ancestor to 
#'                  all descendant tips)
#' @param max.tips:  int, finding common ancestor is very slow for 
#'                   large numbers of tips, down-sampling to a 
#'                   smaller number should obtain the same node 
#'                   at a much faster rate (default 100)
#' @return integer vector indexing edge data frame in obj
#' @export
find.clade <- function(obj, tips, is.mono=TRUE, max.tips=100) {
  unmatched <- !is.element(tips, obj$nodes$label)
  if (any(unmatched)) {
    stop("Error: not all tip labels found in tree layout:")
    cat(tips[!unmatched])
  }
  
  # find most recent common ancestor
  some.tips <- tips
  if (length(some.tips) > max.tips) {
    # down-sample tips to reduce compute time
    # FIXME: this might fail if subtree is very unbalanced
    some.tips <- sample(some.tips, size=max.tips)
  }
  idx <- sapply(some.tips, function(l) which(obj$nodes$label==l))
  
  #t0 <- Sys.time()
  # sample paths from tips to root of tree
  traj <- lapply(idx, function(i) .climb.up.tree(i, obj$edges))
  ##traj <- Reduce(union, traj)
  #print(Sys.time() - t0)

  # this is the shared path to the root  
  common <- Reduce(intersect, traj)
  if (length(common) < 1) {
    stop("Error: Failed to locate common ancestor of tips ", tips)
  }

  # now retrieve non-overlapping paths to root
  idx <- match(tips, obj$nodes$label) #sapply(tips, function(l) which(obj$nodes$label==l))
  traj <- list()  # renamed traj2
  parents <- c()
  for (j in 1:length(idx)) {
    this.traj <- .climb.up.tree(idx[j], edges=obj$edges, parents=parents)
    parents <- union(parents, this.traj)
    traj[[j]] <- this.traj
  }
  clade <- setdiff(Reduce(union, traj), common)
  return(which(is.element(obj$edges$child, c(idx, clade))))
}


#' draw.clade
#' @param obj: an S3 object of class `phyloLayout`
#' @param idx: integer, vector of indices to edges to colour
#' @param col: string, colour specification
#' @export
draw.clade <- function(obj, idx, col='red', ...) { 
  e <- obj$edges[idx, ]
  segments(e$x0, e$y0, e$x1, e$y1, col, ...)

  if (obj$layout == 'rectangular') {
    parents <- sapply(e$parent, function(p) which(obj$edges$child==p))
    root.idx <- which(sapply(parents, length)==0)
    if (length(root.idx)) {
      # clade includes root node, exclude
      segments(x0=e$x0[-root.idx], y0=e$y0[-root.idx], 
               y1=obj$edges$y0[unlist(parents)], col=col, ...)  
    } 
    else {
      segments(x0=e$x0, y0=e$y0, y1=obj$edges$y0[unlist(parents)], 
               col=col, ...)  
    }
  }
  else if (obj$layout == 'radial') {
    nodes <- obj$nodes[e$child,]
    parents <- sapply(e$parent, function(p) which(obj$edges$child==p))
    pnodes <- obj$nodes[obj$edges$child[parents],]
    for (i in 1:nrow(nodes)) {
      draw.arc(x=0, y=0, theta0=nodes$angle[i], theta1=pnodes$angle[i], 
               r0=pnodes$r[i], col=col, ...)      
    }
  }
}


#' get.tips
#' 
#' A recursive function to retrieve a list of indices for all tips that 
#' descend from a given node in the tree.
#' 
#' @param parent:  integer, index of internal node
#' @param obj:  S3 object of class `phyloLayout`
#' @param res:  integer, pass result vector to recursive calls
#' @return an integer vector of row indices for tips in obj$nodes
#' @export
get.tips <- function(parent, obj, res=c()) {
  children <- obj$edges$child[obj$edges$parent==parent]
  for (child in children) {
    if (L$nodes$n.tips[child]==0) {
      res <- c(res, child)
    } else {
      res <- get.tips(child, obj, res)
    }
  }
  return(res)
}

