{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards   #-}

-- Module      : Data.SemVer.Internal
-- Copyright   : (c) 2014-2019 Brendan Hay <brendan.g.hay@gmail.com>
-- License     : This Source Code Form is subject to the terms of
--               the Mozilla Public License, v. 2.0.
--               A copy of the MPL can be found in the LICENSE file or
--               you can obtain it at http://mozilla.org/MPL/2.0/.
-- Maintainer  : Brendan Hay <brendan.g.hay@gmail.com>
-- Stability   : experimental
-- Portability : non-portable (GHC extensions)

module Data.SemVer.Internal where

import           Control.Applicative
import           Control.DeepSeq
import           Control.Monad
import           Data.Attoparsec.Text
import           Data.Function        (on)
import           Data.Hashable
import           Data.List            (intersperse)
import           Data.Monoid
import           Data.Text            (Text)

-- | An opaque type representing a successfully decoded or constructed
-- semantic version. See the related functions and lenses for modification and
-- update.
--
-- * The 'Eq' instance represents exhaustive equality with all
-- components considered.
--
-- * The 'Ord' instance implements the precedence rules from the semantic
-- version specification with metadata being ignored.
data Version = Version
    { _versionMajor   :: !Int
    , _versionMinor   :: !Int
    , _versionPatch   :: !Int
    , _versionRelease :: [Identifier]
    , _versionMeta    :: [Identifier]
    } deriving (Eq, Show)

instance Ord Version where
    compare a b = on compare versions a b <> release
      where
        versions Version{..} =
            [ _versionMajor
            , _versionMinor
            , _versionPatch
            ]

        -- | Compare version releases.
        --
        -- Note: Contrary to 'List's, @[] `compare` [xs]@ equals to @GT@
        release =
            case (_versionRelease a, _versionRelease b) of
                ([], _:_) -> GT
                (_:_, []) -> LT
                (x, y)  -> x `compare` y

instance NFData Version where
    rnf Version{..} =
              rnf _versionMajor
        `seq` rnf _versionMinor
        `seq` rnf _versionPatch
        `seq` rnf _versionRelease
        `seq` rnf _versionMeta

instance Hashable Version where
    hashWithSalt s Version {..} =
        s `hashWithSalt` _versionMajor
          `hashWithSalt` _versionMinor
          `hashWithSalt` _versionPatch
          `hashWithSalt` _versionRelease
          `hashWithSalt` _versionMeta

-- | A type representing an individual identifier from the release
-- or metadata components of a 'Version'.
--
-- * The 'Ord' instance implements precedence according to the semantic version
-- specification, with numeric identifiers being of /lower/ precedence than
-- textual identifiers, otherwise lexicographic ordering is used.
--
-- The functions 'numeric' and 'textual' can be used to construct an 'Identifier'.
data Identifier
    = INum  !Int
    | IText !Text
      deriving (Eq, Show)

instance Ord Identifier where
    compare a b = case (a, b) of
        (INum  x, INum  y) -> x `compare` y
        (IText x, IText y) -> x `compare` y
        (INum  _, _)       -> LT
        (IText _, _)       -> GT

instance NFData Identifier where
    rnf (INum  n) = rnf n
    rnf (IText t) = rnf t

instance Hashable Identifier where
    hashWithSalt s (INum  n) = s `hashWithSalt` (0 :: Int) `hashWithSalt` n
    hashWithSalt s (IText t) = s `hashWithSalt` (1 :: Int) `hashWithSalt` t

identifierParser :: Parser () -> Parser Identifier
identifierParser p =
    either INum IText <$> eitherP (numericParser p) (textualParser p)

numericParser :: Parser () -> Parser Int
numericParser p = nonNegative <* (p <|> endOfInput)

textualParser :: Parser () -> Parser Text
textualParser p = takeWhile1 (inClass "0-9A-Za-z-") <* optional p

nonNegative :: (Show a, Integral a) => Parser a
nonNegative = do
    n <- decimal
    when (n < 0) $
        fail ("Numeric value must be non-negative: " ++ show n)
    return n

-- | An opaque set representing the seperators used to delimit semantic
-- version components.
data Delimiters = Delimiters
    { _delimMinor   :: !Char
    , _delimPatch   :: !Char
    , _delimRelease :: !Char
    , _delimMeta    :: !Char
    , _delimIdent   :: !Char
    } deriving (Eq, Ord, Show)

instance NFData Delimiters where
    rnf Delimiters{..} =
              rnf _delimMinor
        `seq` rnf _delimPatch
        `seq` rnf _delimRelease
        `seq` rnf _delimMeta
        `seq` rnf _delimIdent

toMonoid :: Monoid m
         => (Char -> m)
         -> (Int  -> m)
         -> (Text -> m)
         -> Delimiters
         -> Version
         -> m
toMonoid del int txt Delimiters{..} Version{..} = mconcat
     [ int _versionMajor
     , del _delimMinor
     , int _versionMinor
     , del _delimPatch
     , int _versionPatch
     , f _delimRelease _versionRelease
     , f _delimMeta    _versionMeta
     ]
  where
    f _ [] = mempty
    f c xs = del c <> mconcat (intersperse (del _delimIdent) (map g xs))

    g (INum  n) = int n
    g (IText t) = txt t
{-# INLINE toMonoid #-}