{-# LANGUAGE NoImplicitPrelude, LambdaCase, OverloadedStrings, RecordWildCards #-}
{-|
Module : Yarn.Lock
Description : High-level parser of yarn.lock files
Maintainer : Profpatsch
Stability : experimental

The <https://yarnpkg.com/ Yarn package manager> improves on npm,
because it writes @yarn.lock@ files that contain a complete
version resolution of all dependencies. This way a deterministic
deployment can be guaranteed.
-}
module Yarn.Lock
( T.Lockfile
, parseFile, parse
-- * Errors
, prettyLockfileError
, LockfileError(..), PackageErrorInfo(..)
) where

import Protolude hiding ((<>))
import Data.Semigroup ((<>))
import qualified Data.Text as T
import qualified Data.List.NonEmpty as NE
import qualified Text.Megaparsec as MP
import qualified Data.Either.Validation as V

import qualified Yarn.Lock.Types as T
import qualified Yarn.Lock.File as File
import qualified Yarn.Lock.Parse as Parse

-- | Everything that can go wrong when parsing a 'Lockfile'.
data LockfileError
  = ParseError Text
  -- ^ The initial parsing step failed
  | PackageErrors (NE.NonEmpty PackageErrorInfo)
  -- ^ a package could not be parsed from the AST
  deriving (Show, Eq)

-- | Information about package parsing errors.
data PackageErrorInfo = PackageErrorInfo
  { srcPos :: MP.SourcePos
  -- ^ the position of the package in the original file
  , convErrs :: NE.NonEmpty File.ConversionError
  -- ^ list of reasons for failure
  } deriving (Show, Eq)

-- | Convenience function, combining all parsing steps.
--
-- The resulting 'Lockfile' structure might not yet be optimal,
-- see 'File.fromPackages'.
parseFile :: FilePath -- ^ file to read
          -> IO (Either LockfileError T.Lockfile)
parseFile fp = readFile fp >>= pure . parse fp

-- | For when you want to provide only the file contents.
parse :: FilePath -- ^ name of the input file, used for the parser
      -> Text     -- ^ content of a @yarn.lock@
      -> Either LockfileError T.Lockfile
parse fp = astParse fp >=> toPackages >=> toLockfile

-- | Pretty print a parsing error with sane default formatting.
prettyLockfileError :: LockfileError -> Text
prettyLockfileError = \case
  (ParseError t) -> "Error while parsing the yarn.lock:\n"
    <> T.unlines (indent 2 (T.lines t))
  (PackageErrors errs) -> "Some packages could not be made sense of:\n"
    <> T.unlines (NE.toList $ indent 2 (join $ fmap errText errs))
  where
    indent :: Functor f => Int -> f Text -> f Text
    indent i = fmap (T.replicate i " " <>)
    errText (PackageErrorInfo{..}) =
      (pure $ "Package at " <> (toS $ MP.sourcePosPretty srcPos) <> ":")
      <> indent 2 (fmap convErrText convErrs)
    convErrText = \case
      (File.MissingField t) -> "Field " <> qu t <> " is missing."
      (File.WrongType{..})  -> "Field " <> qu fieldName
                               <> " should be of type " <> fieldType <> "."
      (File.UnknownRemoteType) -> "We don’t know this remote type."
    qu t = "\"" <> t <> "\""

-- helpers
astParse :: FilePath -> Text -> Either LockfileError [Parse.Package]
astParse fp = first (ParseError . toS . MP.errorBundlePretty)
                . MP.parse Parse.packageList fp

toPackages :: [Parse.Package] -> Either LockfileError [T.Keyed T.Package]
toPackages = first PackageErrors . V.validationToEither
                . traverse validatePackage

validatePackage :: Parse.Package
                -> V.Validation (NE.NonEmpty PackageErrorInfo) (T.Keyed T.Package)
validatePackage (T.Keyed keys (pos, fields)) = V.eitherToValidation
  $ bimap (pure . PackageErrorInfo pos) (T.Keyed keys)
    $ File.astToPackage fields

toLockfile :: [T.Keyed T.Package] -> Either LockfileError T.Lockfile
toLockfile = pure . File.fromPackages