{-# LANGUAGE QuasiQuotes       #-}


{-|
Module      : GHCup.Version
Description : Version information and version handling.
Copyright   : (c) Julian Ospald, 2020
License     : LGPL-3.0
Maintainer  : hasufell@hasufell.de
Stability   : experimental
Portability : portable
-}
module GHCup.Version where

import           GHCup.Types
import           Paths_ghcup (version)

import           Data.Version (Version(versionBranch))
import           URI.ByteString
import           URI.ByteString.QQ

import qualified Data.List.NonEmpty            as NE
import qualified Data.Text                     as T
import qualified Data.Versions as V
import Control.Exception.Safe (MonadThrow)
import Data.Text (Text)
import Data.List.NonEmpty (NonEmpty((:|)))
import Data.List (intersperse)
import Control.Monad.Catch (throwM)
import GHCup.Errors (ParseError(..))

-- | This reflects the API version of the YAML.
--
-- Note that when updating this, CI requires that the file exsists AND the same file exists at
-- 'https://www.haskell.org/ghcup/exp/ghcup-<ver>.yaml' with some newlines added.
ghcupURL :: URI
ghcupURL = [uri|https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.7.yaml|]

-- | The current ghcup version.
ghcUpVer :: V.PVP
ghcUpVer = V.PVP . NE.fromList . fmap fromIntegral $ versionBranch version

-- | ghcup version as numeric string.
numericVer :: String
numericVer = T.unpack . V.prettyPVP $ ghcUpVer

versionCmp :: V.Versioning -> VersionCmp -> Bool
versionCmp ver1 (VR_gt ver2)   = ver1 > ver2
versionCmp ver1 (VR_gteq ver2) = ver1 >= ver2
versionCmp ver1 (VR_lt ver2)   = ver1 < ver2
versionCmp ver1 (VR_lteq ver2) = ver1 <= ver2
versionCmp ver1 (VR_eq ver2)   = ver1 == ver2

versionRange :: V.Versioning -> VersionRange -> Bool
versionRange ver' (SimpleRange cmps) = and $ fmap (versionCmp ver') cmps
versionRange ver' (OrRange cmps range) = 
  versionRange ver' (SimpleRange cmps) || versionRange ver' range

pvpToVersion :: MonadThrow m => V.PVP -> Text -> m V.Version
pvpToVersion pvp_ rest =
  either (\_ -> throwM $ ParseError "Couldn't convert PVP to Version") pure . V.version . (<> rest) . V.prettyPVP $ pvp_

-- | Convert a version to a PVP and unparsable rest.
--
-- -- prop> \v -> let (Just (pvp', r)) = versionToPVP v in pvpToVersion pvp' r === Just v
versionToPVP :: MonadThrow m => V.Version -> m (V.PVP, Text)
versionToPVP (V.Version (Just _) _ _ _) = throwM $ ParseError "Unexpected epoch"
versionToPVP v = either (\_ -> (, rest v) <$> alternative v) (pure . (, mempty)) . V.pvp . V.prettyVer $ v
 where
  alternative :: MonadThrow m => V.Version -> m V.PVP
  alternative v' = case NE.takeWhile isDigit (V._vChunks v') of
    [] -> throwM $ ParseError "Couldn't convert Version to PVP"
    xs -> pure $ pvpFromList (unsafeDigit <$> xs)

  rest :: V.Version -> Text
  rest (V.Version _ cs pr me) =
    let chunks = NE.dropWhile isDigit cs
        ver = intersperse (T.pack ".") . chunksAsT $ chunks
        me' = maybe [] (\m -> [T.pack "+",m]) me
        pr' = foldable [] (T.pack "-" :) $ intersperse (T.pack ".") (chunksAsT pr)
        prefix = case (ver, pr', me') of
                   (_:_, _, _) -> T.pack "."
                   _           -> T.pack ""
    in prefix <> mconcat (ver <> pr' <> me')
   where
    chunksAsT :: Functor t => t V.VChunk -> t Text
    chunksAsT = fmap (foldMap f)
      where
        f :: V.VUnit -> Text
        f (V.Digits i) = T.pack $ show i
        f (V.Str s)    = s

    foldable :: Foldable f => f b -> (f a -> f b) -> f a -> f b
    foldable d g f | null f    = d
                   | otherwise = g f



  isDigit :: V.VChunk -> Bool
  isDigit (V.Digits _ :| []) = True
  isDigit _                = False

  unsafeDigit :: V.VChunk -> Int
  unsafeDigit (V.Digits x :| []) = fromIntegral x
  unsafeDigit _ = error "unsafeDigit: wrong input"

pvpFromList :: [Int] -> V.PVP
pvpFromList = V.PVP . NE.fromList . fmap fromIntegral