const { depth } = require('treeverse')

const calcDepFlags = (tree, resetRoot = true) => {
  if (resetRoot) {
    tree.dev = false
    tree.optional = false
    tree.devOptional = false
    tree.peer = false
  }
  const ret = depth({
    tree,
    visit: node => calcDepFlagsStep(node),
    filter: node => node,
    getChildren: (node, tree) =>
      [...tree.edgesOut.values()].map(edge => edge.to),
  })
  return ret
}

const calcDepFlagsStep = (node) => {
  // This rewalk is necessary to handle cases where devDep and optional
  // or normal dependency graphs overlap deep in the dep graph.
  // Since we're only walking through deps that are not already flagged
  // as non-dev/non-optional, it's typically a very shallow traversal

  node.extraneous = false
  resetParents(node, 'extraneous')
  resetParents(node, 'dev')
  resetParents(node, 'peer')
  resetParents(node, 'devOptional')
  resetParents(node, 'optional')

  // for links, map their hierarchy appropriately
  if (node.isLink) {
    // node.target can be null, we check to ensure it's not null before proceeding
    if (node.target == null) {
      return node
    }
    node.target.dev = node.dev
    node.target.optional = node.optional
    node.target.devOptional = node.devOptional
    node.target.peer = node.peer
    return calcDepFlagsStep(node.target)
  }

  node.edgesOut.forEach(({ peer, optional, dev, to }) => {
    // if the dep is missing, then its flags are already maximally unset
    if (!to) {
      return
    }
    // everything with any kind of edge into it is not extraneous
    to.extraneous = false

    // If this is a peer edge, mark the target as peer
    if (peer) {
      to.peer = true
    } else if (to.peer && !hasIncomingPeerEdge(to)) {
      unsetFlag(to, 'peer')
    }

    // devOptional is the *overlap* of the dev and optional tree.
    // however, for convenience and to save an extra rewalk, we leave
    // it set when we are in *either* tree, and then omit it from the
    // package-lock if either dev or optional are set.
    const unsetDevOpt = !node.devOptional && !node.dev && !node.optional && !dev && !optional

    // if we are not in the devOpt tree, then we're also not in
    // either the dev or opt trees
    const unsetDev = unsetDevOpt || !node.dev && !dev
    const unsetOpt = unsetDevOpt || !node.optional && !optional

    if (unsetDevOpt) {
      unsetFlag(to, 'devOptional')
    }

    if (unsetDev) {
      unsetFlag(to, 'dev')
    }

    if (unsetOpt) {
      unsetFlag(to, 'optional')
    }
  })

  return node
}

const hasIncomingPeerEdge = (node) => {
  const target = node.isLink && node.target ? node.target : node
  for (const edge of target.edgesIn) {
    if (edge.type === 'peer') {
      return true
    }
  }
  return false
}

const resetParents = (node, flag) => {
  if (node[flag]) {
    return
  }

  for (let p = node; p && (p === node || p[flag]); p = p.resolveParent) {
    p[flag] = false
  }
}

// typically a short walk, since it only traverses deps that have the flag set.
const unsetFlag = (node, flag) => {
  if (node[flag]) {
    node[flag] = false
    depth({
      tree: node,
      visit: node => {
        node.extraneous = node[flag] = false
        if (node.isLink && node.target) {
          node.target.extraneous = node.target[flag] = false
        }
      },
      getChildren: node => {
        const children = []
        const targetNode = node.isLink && node.target ? node.target : node
        for (const edge of targetNode.edgesOut.values()) {
          if (edge.to?.[flag]) {
            // For the peer flag, only follow peer edges to unset the flag
            // Don't propagate peer flag through prod/dev/optional edges
            if (flag === 'peer') {
              if (edge.type === 'peer') {
                children.push(edge.to)
              }
            } else {
              // For other flags, follow prod edges (and peer edges for non-peer flags)
              if (edge.type === 'prod' || edge.type === 'peer') {
                children.push(edge.to)
              }
            }
          }
        }
        return children
      },
    })
  }
}

module.exports = calcDepFlags
