{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DeriveDataTypeable #-}

-- | Versions for packages.

module Stack.Types.Version
  (Version
  ,Cabal.VersionRange -- TODO in the future should have a newtype wrapper
  ,MajorVersion (..)
  ,getMajorVersion
  ,fromMajorVersion
  ,parseMajorVersionFromString
  ,versionParser
  ,parseVersion
  ,parseVersionFromString
  ,versionString
  ,versionText
  ,toCabalVersion
  ,fromCabalVersion
  ,mkVersion
  ,versionRangeText
  ,withinRange)
  where

import           Control.Applicative
import           Control.DeepSeq
import           Control.Monad.Catch
import           Data.Aeson.Extended
import           Data.Attoparsec.ByteString.Char8
import           Data.Binary (Binary)
import           Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S8
import           Data.Data
import           Data.Hashable
import           Data.List
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Text (Text)
import qualified Data.Text as T
import           Data.Vector.Binary ()
import           Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as V
import           Data.Word
import           Distribution.Text (disp)
import qualified Distribution.Version as Cabal
import           GHC.Generics
import           Language.Haskell.TH
import           Language.Haskell.TH.Syntax
import           Prelude -- Fix warning: Word in Prelude from base-4.8.
import           Text.PrettyPrint (render)

-- | A parse fail.
data VersionParseFail =
  VersionParseFail ByteString
  | NotAMajorVersion Version
  deriving (Typeable)
instance Exception VersionParseFail
instance Show VersionParseFail where
    show (VersionParseFail bs) = "Invalid version: " ++ show bs
    show (NotAMajorVersion v) = concat
        [ "Not a major version: "
        , versionString v
        , ", expecting exactly two numbers (e.g. 7.10)"
        ]

-- | A package version.
newtype Version =
  Version {unVersion :: Vector Word}
  deriving (Eq,Ord,Typeable,Data,Generic,Binary,NFData)

-- | The first two components of a version.
data MajorVersion = MajorVersion !Word !Word
    deriving (Typeable, Eq, Ord)
instance Show MajorVersion where
    show (MajorVersion x y) = concat [show x, ".", show y]
instance ToJSON MajorVersion where
    toJSON = toJSON . fromMajorVersion

-- | Parse major version from @String@
parseMajorVersionFromString :: MonadThrow m => String -> m MajorVersion
parseMajorVersionFromString s = do
    Version v <- parseVersionFromString s
    if V.length v == 2
        then return $ getMajorVersion (Version v)
        else throwM $ NotAMajorVersion (Version v)

instance FromJSON MajorVersion where
    parseJSON = withText "MajorVersion"
              $ either (fail . show) return
              . parseMajorVersionFromString
              . T.unpack
instance FromJSON a => FromJSON (Map MajorVersion a) where
    parseJSON val = do
        m <- parseJSON val
        fmap Map.fromList $ mapM go $ Map.toList m
      where
        go (k, v) = do
            k' <- either (fail . show) return $ parseMajorVersionFromString k
            return (k', v)

-- | Returns the first two components, defaulting to 0 if not present
getMajorVersion :: Version -> MajorVersion
getMajorVersion (Version v) =
    case V.length v of
        0 -> MajorVersion 0 0
        1 -> MajorVersion (V.head v) 0
        _ -> MajorVersion (V.head v) (v V.! 1)

-- | Convert a two-component version into a @Version@
fromMajorVersion :: MajorVersion -> Version
fromMajorVersion (MajorVersion x y) = Version $ V.fromList [x, y]

instance Hashable Version where
  hashWithSalt i = hashWithSalt i . V.toList . unVersion

instance Lift Version where
  lift (Version n) =
    appE (conE 'Version)
         (appE (varE 'V.fromList)
               (listE (map (litE . IntegerL . fromIntegral)
                           (V.toList n))))

instance Show Version where
  show (Version v) =
    intercalate "."
                (map show (V.toList v))

instance ToJSON Version where
  toJSON = toJSON . versionText
instance FromJSON Version where
  parseJSON j =
    do s <- parseJSON j
       case parseVersionFromString s of
         Nothing ->
           fail ("Couldn't parse package version: " ++ s)
         Just ver -> return ver

-- | Attoparsec parser for a package version from bytestring.
versionParser :: Parser Version
versionParser =
  do ls <- ((:) <$> num <*> many num')
     let !v = V.fromList ls
     return (Version v)
  where num = decimal
        num' = point *> num
        point = satisfy (== '.')

-- | Convenient way to parse a package version from a bytestring.
parseVersion :: MonadThrow m => ByteString -> m Version
parseVersion x = go x
  where go =
          either (const (throwM (VersionParseFail x))) return .
          parseOnly (versionParser <* endOfInput)

-- | Migration function.
parseVersionFromString :: MonadThrow m => String -> m Version
parseVersionFromString =
  parseVersion . S8.pack

-- | Get a string representation of a package version.
versionString :: Version -> String
versionString (Version v) =
  intercalate "."
              (map show (V.toList v))

-- | Get a string representation of a package version.
versionText :: Version -> Text
versionText (Version v) =
  T.intercalate
    "."
    (map (T.pack . show)
         (V.toList v))

-- | Convert to a Cabal version.
toCabalVersion :: Version -> Cabal.Version
toCabalVersion (Version v) =
  Cabal.Version (map fromIntegral (V.toList v)) []

-- | Convert from a Cabal version.
fromCabalVersion :: Cabal.Version -> Version
fromCabalVersion (Cabal.Version vs _) =
  let !v = V.fromList (map fromIntegral vs)
  in Version v

-- | Make a package version.
mkVersion :: String -> Q Exp
mkVersion s =
  case parseVersionFromString s of
    Nothing -> error ("Invalid package version: " ++ show s)
    Just pn -> [|pn|]

-- | Display a version range
versionRangeText :: Cabal.VersionRange -> Text
versionRangeText = T.pack . render . disp

-- | Check if a version is within a version range.
withinRange :: Version -> Cabal.VersionRange -> Bool
withinRange v r = toCabalVersion v `Cabal.withinRange` r
