{-# LANGUAGE NoImplicitPrelude #-}
{-|
Module : Yarn.Lock.Helpers
Description : Helpers for modifying Lockfiles
Maintainer : Profpatsch
Stability : experimental

Freshly parsed 'Lockfile's are often not directly usable
e.g. because they still can contain cycles. This module
provides helpers for modifying them.
-}
module Yarn.Lock.Helpers
( decycle
) where

import Protolude
import qualified Data.List as L
import GHC.Stack (HasCallStack)

import qualified Data.MultiKeyedMap as MKM

import Yarn.Lock.Types


-- | Takes a 'Lockfile' and removes dependency cycles.
--
-- Node packages often contain those and the yarn lockfile
-- does not yet eliminate them, which may lead to infinite
-- recursions.
--
-- Invariant: Every dependency entry in each package in the
-- 'Lockfile' *must* point to an existing key, otherwise
-- the function crashes.
decycle :: HasCallStack => Lockfile -> Lockfile
decycle lf = goFold [] lf (MKM.keys lf)
  -- TODO: probably rewrite with State
  where
    -- | fold over all package keys, passing the lockfile
    goFold seen lf' pkeys =
      foldl' (\lf'' pkey -> go (pkey:seen) lf'') lf' pkeys
    -- | We get a stack of already seen packages
    -- and filter out any dependencies we already saw.
    go :: [PackageKey] -> Lockfile -> Lockfile
    go seen@(we:_) lf' =
      let ourPkg = lf' MKM.! we
          -- old deps minus the already seen ones
          -- TODO make handling of opt pkgs less of a duplication
          newDeps = dependencies ourPkg L.\\ seen
          newOptDeps = optionalDependencies ourPkg L.\\ seen
          -- we update the pkg with the cleaned dependencies
          lf'' = MKM.insert we (ourPkg { dependencies = newDeps
                               , optionalDependencies = newOptDeps }) lf'
      -- finally we do the same for all remaining deps
      in goFold seen lf'' $ newDeps ++ newOptDeps
    go [] _ = panic $ toS "should not happen!"